From e3aa8a06175e752496a04b3dc21f0ee29f105d8b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 7 May 2018 15:15:01 -0700 Subject: [PATCH 0001/1556] 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 0002/1556] 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 0003/1556] 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 0004/1556] 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 0005/1556] 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 0006/1556] 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 0007/1556] 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 0008/1556] 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 0009/1556] 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 0010/1556] 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 0011/1556] 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 0012/1556] 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"> * * * *

Overriding the layout file

@@ -154,7 +158,15 @@ import java.util.Locale; *
    *
  • Type: {@link TextView} *
+ *
  • {@code exo_progress_placeholder} - A placeholder that's replaced with the inflated + * {@link DefaultTimeBar}. Ignored if an {@code exo_progress} view exists. + *
      + *
    • Type: {@link View} + *
    *
  • {@code exo_progress} - Time bar that's updated during playback and allows seeking. + * {@link DefaultTimeBar} attributes set on the PlayerControlView will not be automatically + * propagated through to this instance. If a view exists with this id, any {@code + * exo_progress_placeholder} view will be ignored. *
      *
    • Type: {@link TimeBar} *
    @@ -330,9 +342,27 @@ public class PlayerControlView extends FrameLayout { LayoutInflater.from(context).inflate(controllerLayoutId, this); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + TimeBar customTimeBar = findViewById(R.id.exo_progress); + View timeBarPlaceholder = findViewById(R.id.exo_progress_placeholder); + if (customTimeBar != null) { + timeBar = customTimeBar; + } else if (timeBarPlaceholder != null) { + // Propagate attrs as timebarAttrs so that DefaultTimeBar's custom attributes are transferred, + // but standard attributes (e.g. background) are not. + DefaultTimeBar defaultTimeBar = new DefaultTimeBar(context, null, 0, playbackAttrs); + defaultTimeBar.setId(R.id.exo_progress); + defaultTimeBar.setLayoutParams(timeBarPlaceholder.getLayoutParams()); + ViewGroup parent = ((ViewGroup) timeBarPlaceholder.getParent()); + int timeBarIndex = parent.indexOfChild(timeBarPlaceholder); + parent.removeView(timeBarPlaceholder); + parent.addView(defaultTimeBar, timeBarIndex); + timeBar = defaultTimeBar; + } else { + timeBar = null; + } durationView = findViewById(R.id.exo_duration); positionView = findViewById(R.id.exo_position); - timeBar = findViewById(R.id.exo_progress); + if (timeBar != null) { timeBar.addListener(componentListener); } 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 21467c3e25..5bb8324780 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 @@ -162,9 +162,10 @@ import java.util.List; *
  • Corresponding method: None *
  • Default: {@code R.layout.exo_player_control_view} * - *
  • All attributes that can be set on a {@link PlayerControlView} can also be set on a - * PlayerView, and will be propagated to the inflated {@link PlayerControlView} unless the - * layout is overridden to specify a custom {@code exo_controller} (see below). + *
  • All attributes that can be set on {@link PlayerControlView} and {@link DefaultTimeBar} can + * also be set on a PlayerView, and will be propagated to the inflated {@link + * PlayerControlView} unless the layout is overridden to specify a custom {@code + * exo_controller} (see below). * * *

    Overriding the layout file

    @@ -214,9 +215,10 @@ import java.util.List; *
  • Type: {@link View} * *
  • {@code exo_controller} - An already inflated {@link PlayerControlView}. Allows use - * of a custom extension of {@link PlayerControlView}. Note that attributes such as {@code - * rewind_increment} will not be automatically propagated through to this instance. If a view - * exists with this id, any {@code exo_controller_placeholder} view will be ignored. + * of a custom extension of {@link PlayerControlView}. {@link PlayerControlView} and {@link + * DefaultTimeBar} attributes set on the PlayerView will not be automatically propagated + * through to this instance. If a view exists with this id, any {@code + * exo_controller_placeholder} view will be ignored. *
      *
    • Type: {@link PlayerControlView} *
    @@ -456,8 +458,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider this.controller = customController; } else if (controllerPlaceholder != null) { // Propagate attrs as playbackAttrs so that PlayerControlView's custom attributes are - // transferred, but standard FrameLayout attributes (e.g. background) are not. + // transferred, but standard attributes (e.g. background) are not. this.controller = new PlayerControlView(context, null, 0, attrs); + controller.setId(R.id.exo_controller); controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); int controllerIndex = parent.indexOfChild(controllerPlaceholder); diff --git a/library/ui/src/main/res/layout/exo_playback_control_view.xml b/library/ui/src/main/res/layout/exo_playback_control_view.xml index ed2fb8e2b2..027e57ee92 100644 --- a/library/ui/src/main/res/layout/exo_playback_control_view.xml +++ b/library/ui/src/main/res/layout/exo_playback_control_view.xml @@ -76,8 +76,7 @@ android:includeFontPadding="false" android:textColor="#FFBEBEBE"/> - diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 27e6a5b3b8..706fba0e0b 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -31,18 +31,36 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -58,9 +76,11 @@ - + + - + + @@ -69,6 +89,20 @@ + + + + + + + + + + + + + + @@ -83,22 +117,36 @@ + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/library/ui/src/main/res/values/ids.xml b/library/ui/src/main/res/values/ids.xml index e57301f946..17b55cd731 100644 --- a/library/ui/src/main/res/values/ids.xml +++ b/library/ui/src/main/res/values/ids.xml @@ -33,6 +33,7 @@ + From a727acd29280969a7c11b3ee5db73257a11e5970 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 May 2019 18:14:38 +0100 Subject: [PATCH 1294/1556] Remove nullness test blacklist for RTMP extension PiperOrigin-RevId: 249274122 --- .../android/exoplayer2/ext/rtmp/RtmpDataSource.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java index 272a8d1eb4..bf5b8e9261 100644 --- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java +++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.ext.rtmp; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -34,8 +36,8 @@ public final class RtmpDataSource extends BaseDataSource { ExoPlayerLibraryInfo.registerModule("goog.exo.rtmp"); } - private RtmpClient rtmpClient; - private Uri uri; + @Nullable private RtmpClient rtmpClient; + @Nullable private Uri uri; public RtmpDataSource() { super(/* isNetwork= */ true); @@ -66,7 +68,7 @@ public final class RtmpDataSource extends BaseDataSource { @Override public int read(byte[] buffer, int offset, int readLength) throws IOException { - int bytesRead = rtmpClient.read(buffer, offset, readLength); + int bytesRead = castNonNull(rtmpClient).read(buffer, offset, readLength); if (bytesRead == -1) { return C.RESULT_END_OF_INPUT; } @@ -87,6 +89,7 @@ public final class RtmpDataSource extends BaseDataSource { } @Override + @Nullable public Uri getUri() { return uri; } From 52888ab55b6bc8c8ebe8f6723937efe27c9a6a4d Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 May 2019 18:21:42 +0100 Subject: [PATCH 1295/1556] Remove CronetEngineWrapper from nullness test blacklist PiperOrigin-RevId: 249275623 --- .../exoplayer2/ext/cronet/CronetEngineWrapper.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java index 270c1f6323..7d549be7cb 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.cronet; import android.content.Context; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; @@ -37,8 +38,8 @@ public final class CronetEngineWrapper { private static final String TAG = "CronetEngineWrapper"; - private final CronetEngine cronetEngine; - private final @CronetEngineSource int cronetEngineSource; + @Nullable private final CronetEngine cronetEngine; + @CronetEngineSource private final int cronetEngineSource; /** * Source of {@link CronetEngine}. One of {@link #SOURCE_NATIVE}, {@link #SOURCE_GMS}, {@link @@ -144,7 +145,8 @@ public final class CronetEngineWrapper { * * @return A {@link CronetEngineSource} value. */ - public @CronetEngineSource int getCronetEngineSource() { + @CronetEngineSource + public int getCronetEngineSource() { return cronetEngineSource; } @@ -153,13 +155,14 @@ public final class CronetEngineWrapper { * * @return The CronetEngine, or null if no CronetEngine is available. */ + @Nullable /* package */ CronetEngine getCronetEngine() { return cronetEngine; } private static class CronetProviderComparator implements Comparator { - private final String gmsCoreCronetName; + @Nullable private final String gmsCoreCronetName; private final boolean preferGMSCoreCronet; // Multi-catch can only be used for API 19+ in this case. From 3efe3205354125d1d28bd41932278a8100a841cd Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 May 2019 18:23:51 +0100 Subject: [PATCH 1296/1556] Remove deprecated DataSource constructors PiperOrigin-RevId: 249276112 --- .../exoplayer2/ext/rtmp/RtmpDataSource.java | 14 --- .../exoplayer2/upstream/AssetDataSource.java | 14 --- .../upstream/ContentDataSource.java | 14 --- .../upstream/DefaultDataSource.java | 85 +----------------- .../upstream/DefaultDataSourceFactory.java | 4 +- .../upstream/DefaultHttpDataSource.java | 87 +------------------ .../exoplayer2/upstream/FileDataSource.java | 12 --- .../upstream/RawResourceDataSource.java | 14 --- .../exoplayer2/upstream/UdpDataSource.java | 44 ---------- 9 files changed, 7 insertions(+), 281 deletions(-) diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java index bf5b8e9261..366d1c120d 100644 --- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java +++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java @@ -18,13 +18,11 @@ package com.google.android.exoplayer2.ext.rtmp; import static com.google.android.exoplayer2.util.Util.castNonNull; import android.net.Uri; -import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.upstream.BaseDataSource; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.upstream.TransferListener; import java.io.IOException; import net.butterflytv.rtmp_client.RtmpClient; import net.butterflytv.rtmp_client.RtmpClient.RtmpIOException; @@ -43,18 +41,6 @@ public final class RtmpDataSource extends BaseDataSource { super(/* isNetwork= */ true); } - /** - * @param listener An optional listener. - * @deprecated Use {@link #RtmpDataSource()} and {@link #addTransferListener(TransferListener)}. - */ - @Deprecated - public RtmpDataSource(@Nullable TransferListener listener) { - this(); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public long open(DataSpec dataSpec) throws RtmpIOException { transferInitializing(dataSpec); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java index 9224e14d4a..eeb0f1c957 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java @@ -51,20 +51,6 @@ public final class AssetDataSource extends BaseDataSource { this.assetManager = context.getAssets(); } - /** - * @param context A context. - * @param listener An optional listener. - * @deprecated Use {@link #AssetDataSource(Context)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - public AssetDataSource(Context context, @Nullable TransferListener listener) { - this(context); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public long open(DataSpec dataSpec) throws AssetDataSourceException { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index c723d3f1ca..8df69ffb7a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -57,20 +57,6 @@ public final class ContentDataSource extends BaseDataSource { this.resolver = context.getContentResolver(); } - /** - * @param context A context. - * @param listener An optional listener. - * @deprecated Use {@link #ContentDataSource(Context)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - public ContentDataSource(Context context, @Nullable TransferListener listener) { - this(context); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public long open(DataSpec dataSpec) throws ContentDataSourceException { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java index 8b4107850c..aa13baa03e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -43,9 +43,9 @@ import java.util.Map; * explicit dependency on ExoPlayer's RTMP extension. *
  • data: For parsing data inlined in the URI as defined in RFC 2397. *
  • http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4), - * if constructed using {@link #DefaultDataSource(Context, TransferListener, String, - * boolean)}, or any other schemes supported by a base data source if constructed using {@link - * #DefaultDataSource(Context, TransferListener, DataSource)}. + * if constructed using {@link #DefaultDataSource(Context, String, boolean)}, or any other + * schemes supported by a base data source if constructed using {@link + * #DefaultDataSource(Context, DataSource)}. * */ public final class DefaultDataSource implements DataSource { @@ -131,85 +131,6 @@ public final class DefaultDataSource implements DataSource { transferListeners = new ArrayList<>(); } - /** - * Constructs a new instance, optionally configured to follow cross-protocol redirects. - * - * @param context A context. - * @param listener An optional listener. - * @param userAgent The User-Agent to use when requesting remote data. - * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP - * to HTTPS and vice versa) are enabled when fetching remote data. - * @deprecated Use {@link #DefaultDataSource(Context, String, boolean)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public DefaultDataSource( - Context context, - @Nullable TransferListener listener, - String userAgent, - boolean allowCrossProtocolRedirects) { - this(context, listener, userAgent, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, - DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, allowCrossProtocolRedirects); - } - - /** - * Constructs a new instance, optionally configured to follow cross-protocol redirects. - * - * @param context A context. - * @param listener An optional listener. - * @param userAgent The User-Agent to use when requesting remote data. - * @param connectTimeoutMillis The connection timeout that should be used when requesting remote - * data, in milliseconds. A timeout of zero is interpreted as an infinite timeout. - * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in - * milliseconds. A timeout of zero is interpreted as an infinite timeout. - * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP - * to HTTPS and vice versa) are enabled when fetching remote data. - * @deprecated Use {@link #DefaultDataSource(Context, String, int, int, boolean)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public DefaultDataSource( - Context context, - @Nullable TransferListener listener, - String userAgent, - int connectTimeoutMillis, - int readTimeoutMillis, - boolean allowCrossProtocolRedirects) { - this( - context, - listener, - new DefaultHttpDataSource( - userAgent, - /* contentTypePredicate= */ null, - listener, - connectTimeoutMillis, - readTimeoutMillis, - allowCrossProtocolRedirects, - /* defaultRequestProperties= */ null)); - } - - /** - * Constructs a new instance that delegates to a provided {@link DataSource} for URI schemes other - * than file, asset and content. - * - * @param context A context. - * @param listener An optional listener. - * @param baseDataSource A {@link DataSource} to use for URI schemes other than file, asset and - * content. This {@link DataSource} should normally support at least http(s). - * @deprecated Use {@link #DefaultDataSource(Context, DataSource)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - public DefaultDataSource( - Context context, @Nullable TransferListener listener, DataSource baseDataSource) { - this(context, baseDataSource); - if (listener != null) { - transferListeners.add(listener); - } - } - @Override public void addTransferListener(TransferListener transferListener) { baseDataSource.addTransferListener(transferListener); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java index 9639b4ede1..a5dfad72f0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java @@ -51,7 +51,7 @@ public final class DefaultDataSourceFactory implements Factory { * @param context A context. * @param baseDataSourceFactory A {@link Factory} to be used to create a base {@link DataSource} * for {@link DefaultDataSource}. - * @see DefaultDataSource#DefaultDataSource(Context, TransferListener, DataSource) + * @see DefaultDataSource#DefaultDataSource(Context, DataSource) */ public DefaultDataSourceFactory(Context context, DataSource.Factory baseDataSourceFactory) { this(context, /* listener= */ null, baseDataSourceFactory); @@ -62,7 +62,7 @@ public final class DefaultDataSourceFactory implements Factory { * @param listener An optional listener. * @param baseDataSourceFactory A {@link Factory} to be used to create a base {@link DataSource} * for {@link DefaultDataSource}. - * @see DefaultDataSource#DefaultDataSource(Context, TransferListener, DataSource) + * @see DefaultDataSource#DefaultDataSource(Context, DataSource) */ public DefaultDataSourceFactory( Context context, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index 66036b7a84..65b65efe2c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -47,8 +47,8 @@ import java.util.regex.Pattern; * *

    By default this implementation will not follow cross-protocol redirects (i.e. redirects from * HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the {@link - * #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean, - * RequestProperties)} constructor and passing {@code true} as the second last argument. + * #DefaultHttpDataSource(String, Predicate, int, int, boolean, RequestProperties)} constructor and + * passing {@code true} as the second last argument. */ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSource { @@ -164,89 +164,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou this.defaultRequestProperties = defaultRequestProperties; } - /** - * @param userAgent The User-Agent string that should be used. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link - * #open(DataSpec)}. - * @param listener An optional listener. - * @deprecated Use {@link #DefaultHttpDataSource(String, Predicate)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public DefaultHttpDataSource( - String userAgent, - @Nullable Predicate contentTypePredicate, - @Nullable TransferListener listener) { - this(userAgent, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS, - DEFAULT_READ_TIMEOUT_MILLIS); - } - - /** - * @param userAgent The User-Agent string that should be used. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link - * #open(DataSpec)}. - * @param listener An optional listener. - * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is - * interpreted as an infinite timeout. - * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as - * an infinite timeout. - * @deprecated Use {@link #DefaultHttpDataSource(String, Predicate, int, int)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public DefaultHttpDataSource( - String userAgent, - @Nullable Predicate contentTypePredicate, - @Nullable TransferListener listener, - int connectTimeoutMillis, - int readTimeoutMillis) { - this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false, - null); - } - - /** - * @param userAgent The User-Agent string that should be used. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link - * #open(DataSpec)}. - * @param listener An optional listener. - * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is - * interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use the - * default value. - * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as - * an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value. - * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP - * to HTTPS and vice versa) are enabled. - * @param defaultRequestProperties The default request properties to be sent to the server as HTTP - * headers or {@code null} if not required. - * @deprecated Use {@link #DefaultHttpDataSource(String, Predicate, int, int, boolean, - * RequestProperties)} and {@link #addTransferListener(TransferListener)}. - */ - @Deprecated - public DefaultHttpDataSource( - String userAgent, - @Nullable Predicate contentTypePredicate, - @Nullable TransferListener listener, - int connectTimeoutMillis, - int readTimeoutMillis, - boolean allowCrossProtocolRedirects, - @Nullable RequestProperties defaultRequestProperties) { - this( - userAgent, - contentTypePredicate, - connectTimeoutMillis, - readTimeoutMillis, - allowCrossProtocolRedirects, - defaultRequestProperties); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public @Nullable Uri getUri() { return connection == null ? null : Uri.parse(connection.getURL().toString()); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java index 3cfdc4812b..cead366360 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java @@ -45,18 +45,6 @@ public final class FileDataSource extends BaseDataSource { super(/* isNetwork= */ false); } - /** - * @param listener An optional listener. - * @deprecated Use {@link #FileDataSource()} and {@link #addTransferListener(TransferListener)} - */ - @Deprecated - public FileDataSource(@Nullable TransferListener listener) { - this(); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public long open(DataSpec dataSpec) throws FileDataSourceException { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java index 1f0313594b..7b70bcc5c4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java @@ -78,20 +78,6 @@ public final class RawResourceDataSource extends BaseDataSource { this.resources = context.getResources(); } - /** - * @param context A context. - * @param listener An optional listener. - * @deprecated Use {@link #RawResourceDataSource(Context)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - public RawResourceDataSource(Context context, @Nullable TransferListener listener) { - this(context); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public long open(DataSpec dataSpec) throws RawResourceDataSourceException { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java index e7aab31cc2..fcfeef3fb4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java @@ -88,50 +88,6 @@ public final class UdpDataSource extends BaseDataSource { packet = new DatagramPacket(packetBuffer, 0, maxPacketSize); } - /** - * Constructs a new instance. - * - * @param listener An optional listener. - * @deprecated Use {@link #UdpDataSource()} and {@link #addTransferListener(TransferListener)}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public UdpDataSource(@Nullable TransferListener listener) { - this(listener, DEFAULT_MAX_PACKET_SIZE); - } - - /** - * Constructs a new instance. - * - * @param listener An optional listener. - * @param maxPacketSize The maximum datagram packet size, in bytes. - * @deprecated Use {@link #UdpDataSource(int)} and {@link #addTransferListener(TransferListener)}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public UdpDataSource(@Nullable TransferListener listener, int maxPacketSize) { - this(listener, maxPacketSize, DEFAULT_SOCKET_TIMEOUT_MILLIS); - } - - /** - * Constructs a new instance. - * - * @param listener An optional listener. - * @param maxPacketSize The maximum datagram packet size, in bytes. - * @param socketTimeoutMillis The socket timeout in milliseconds. A timeout of zero is interpreted - * as an infinite timeout. - * @deprecated Use {@link #UdpDataSource(int, int)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - public UdpDataSource( - @Nullable TransferListener listener, int maxPacketSize, int socketTimeoutMillis) { - this(maxPacketSize, socketTimeoutMillis); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public long open(DataSpec dataSpec) throws UdpDataSourceException { uri = dataSpec.uri; From 8669d6dc102f7a8c99e8fed6f5d29b973fa8503a Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 May 2019 20:04:42 +0100 Subject: [PATCH 1297/1556] Fix missing import PiperOrigin-RevId: 249298093 --- .../com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java index 366d1c120d..587e310d64 100644 --- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java +++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.ext.rtmp; import static com.google.android.exoplayer2.util.Util.castNonNull; import android.net.Uri; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.upstream.BaseDataSource; From 21be28431840f8adf73ea1cbed3e2c7fa7cf86c7 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 May 2019 22:36:17 +0100 Subject: [PATCH 1298/1556] Replace runtime lookups of string integer codes Make these values compile-time constants, which can be inlined. PiperOrigin-RevId: 249327464 --- .../android/exoplayer2/audio/WavUtil.java | 8 +- .../extractor/flv/FlvExtractor.java | 3 +- .../extractor/mp3/Mp3Extractor.java | 7 +- .../exoplayer2/extractor/mp4/Atom.java | 221 +++++++++--------- .../exoplayer2/extractor/mp4/AtomParsers.java | 16 +- .../extractor/mp4/FragmentedMp4Extractor.java | 2 +- .../extractor/mp4/MetadataUtil.java | 59 +++-- .../extractor/mp4/Mp4Extractor.java | 3 +- .../exoplayer2/extractor/mp4/Sniffer.java | 55 +++-- .../extractor/ogg/OggPageHeader.java | 3 +- .../exoplayer2/extractor/ogg/OpusReader.java | 3 +- .../extractor/rawcc/RawCcExtractor.java | 3 +- .../exoplayer2/extractor/ts/Ac3Extractor.java | 3 +- .../exoplayer2/extractor/ts/Ac4Extractor.java | 3 +- .../extractor/ts/AdtsExtractor.java | 3 +- .../exoplayer2/extractor/ts/TsExtractor.java | 8 +- .../extractor/wav/WavHeaderReader.java | 7 +- .../exoplayer2/metadata/id3/Id3Decoder.java | 6 +- .../android/exoplayer2/text/cea/CeaUtil.java | 3 +- .../exoplayer2/text/tx3g/Tx3gDecoder.java | 4 +- .../text/webvtt/Mp4WebvttDecoder.java | 6 +- .../video/spherical/ProjectionDecoder.java | 12 +- 22 files changed, 212 insertions(+), 226 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java index 473a91fedf..f5cabf7c30 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java @@ -23,13 +23,13 @@ import com.google.android.exoplayer2.util.Util; public final class WavUtil { /** Four character code for "RIFF". */ - public static final int RIFF_FOURCC = Util.getIntegerCodeForString("RIFF"); + public static final int RIFF_FOURCC = 0x52494646; /** Four character code for "WAVE". */ - public static final int WAVE_FOURCC = Util.getIntegerCodeForString("WAVE"); + public static final int WAVE_FOURCC = 0x57415645; /** Four character code for "fmt ". */ - public static final int FMT_FOURCC = Util.getIntegerCodeForString("fmt "); + public static final int FMT_FOURCC = 0x666d7420; /** Four character code for "data". */ - public static final int DATA_FOURCC = Util.getIntegerCodeForString("data"); + public static final int DATA_FOURCC = 0x64617461; /** WAVE type value for integer PCM audio data. */ private static final int TYPE_PCM = 0x0001; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index 0a2c0c46f6..15b36157fb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -24,7 +24,6 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -64,7 +63,7 @@ public final class FlvExtractor implements Extractor { private static final int TAG_TYPE_SCRIPT_DATA = 18; // FLV container identifier. - private static final int FLV_TAG = Util.getIntegerCodeForString("FLV"); + private static final int FLV_TAG = 0x00464c56; private final ParsableByteArray scratch; private final ParsableByteArray headerBuffer; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index c65ad0bc67..8f13cfaa11 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -35,7 +35,6 @@ import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; import com.google.android.exoplayer2.metadata.id3.MlltFrame; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; import java.lang.annotation.Documented; @@ -95,9 +94,9 @@ public final class Mp3Extractor implements Extractor { */ private static final int MPEG_AUDIO_HEADER_MASK = 0xFFFE0C00; - private static final int SEEK_HEADER_XING = Util.getIntegerCodeForString("Xing"); - private static final int SEEK_HEADER_INFO = Util.getIntegerCodeForString("Info"); - private static final int SEEK_HEADER_VBRI = Util.getIntegerCodeForString("VBRI"); + private static final int SEEK_HEADER_XING = 0x58696e67; + private static final int SEEK_HEADER_INFO = 0x496e666f; + private static final int SEEK_HEADER_VBRI = 0x56425249; private static final int SEEK_HEADER_UNSET = 0; @Flags private final int flags; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index f66c1f5d2c..9bfe383169 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.mp4; import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -51,334 +50,334 @@ import java.util.List; public static final int EXTENDS_TO_END_SIZE = 0; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ftyp = Util.getIntegerCodeForString("ftyp"); + public static final int TYPE_ftyp = 0x66747970; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_avc1 = Util.getIntegerCodeForString("avc1"); + public static final int TYPE_avc1 = 0x61766331; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_avc3 = Util.getIntegerCodeForString("avc3"); + public static final int TYPE_avc3 = 0x61766333; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_avcC = Util.getIntegerCodeForString("avcC"); + public static final int TYPE_avcC = 0x61766343; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_hvc1 = Util.getIntegerCodeForString("hvc1"); + public static final int TYPE_hvc1 = 0x68766331; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_hev1 = Util.getIntegerCodeForString("hev1"); + public static final int TYPE_hev1 = 0x68657631; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_hvcC = Util.getIntegerCodeForString("hvcC"); + public static final int TYPE_hvcC = 0x68766343; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_vp08 = Util.getIntegerCodeForString("vp08"); + public static final int TYPE_vp08 = 0x76703038; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09"); + public static final int TYPE_vp09 = 0x76703039; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC"); + public static final int TYPE_vpcC = 0x76706343; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_av01 = Util.getIntegerCodeForString("av01"); + public static final int TYPE_av01 = 0x61763031; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_av1C = Util.getIntegerCodeForString("av1C"); + public static final int TYPE_av1C = 0x61763143; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dvav = Util.getIntegerCodeForString("dvav"); + public static final int TYPE_dvav = 0x64766176; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dva1 = Util.getIntegerCodeForString("dva1"); + public static final int TYPE_dva1 = 0x64766131; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dvhe = Util.getIntegerCodeForString("dvhe"); + public static final int TYPE_dvhe = 0x64766865; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dvh1 = Util.getIntegerCodeForString("dvh1"); + public static final int TYPE_dvh1 = 0x64766831; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dvcC = Util.getIntegerCodeForString("dvcC"); + public static final int TYPE_dvcC = 0x64766343; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dvvC = Util.getIntegerCodeForString("dvvC"); + public static final int TYPE_dvvC = 0x64767643; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_s263 = Util.getIntegerCodeForString("s263"); + public static final int TYPE_s263 = 0x73323633; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_d263 = Util.getIntegerCodeForString("d263"); + public static final int TYPE_d263 = 0x64323633; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mdat = Util.getIntegerCodeForString("mdat"); + public static final int TYPE_mdat = 0x6d646174; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mp4a = Util.getIntegerCodeForString("mp4a"); + public static final int TYPE_mp4a = 0x6d703461; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE__mp3 = Util.getIntegerCodeForString(".mp3"); + public static final int TYPE__mp3 = 0x2e6d7033; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_wave = Util.getIntegerCodeForString("wave"); + public static final int TYPE_wave = 0x77617665; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_lpcm = Util.getIntegerCodeForString("lpcm"); + public static final int TYPE_lpcm = 0x6c70636d; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sowt = Util.getIntegerCodeForString("sowt"); + public static final int TYPE_sowt = 0x736f7774; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ac_3 = Util.getIntegerCodeForString("ac-3"); + public static final int TYPE_ac_3 = 0x61632d33; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dac3 = Util.getIntegerCodeForString("dac3"); + public static final int TYPE_dac3 = 0x64616333; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ec_3 = Util.getIntegerCodeForString("ec-3"); + public static final int TYPE_ec_3 = 0x65632d33; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dec3 = Util.getIntegerCodeForString("dec3"); + public static final int TYPE_dec3 = 0x64656333; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ac_4 = Util.getIntegerCodeForString("ac-4"); + public static final int TYPE_ac_4 = 0x61632d34; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dac4 = Util.getIntegerCodeForString("dac4"); + public static final int TYPE_dac4 = 0x64616334; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dtsc = Util.getIntegerCodeForString("dtsc"); + public static final int TYPE_dtsc = 0x64747363; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dtsh = Util.getIntegerCodeForString("dtsh"); + public static final int TYPE_dtsh = 0x64747368; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dtsl = Util.getIntegerCodeForString("dtsl"); + public static final int TYPE_dtsl = 0x6474736c; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dtse = Util.getIntegerCodeForString("dtse"); + public static final int TYPE_dtse = 0x64747365; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ddts = Util.getIntegerCodeForString("ddts"); + public static final int TYPE_ddts = 0x64647473; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_tfdt = Util.getIntegerCodeForString("tfdt"); + public static final int TYPE_tfdt = 0x74666474; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_tfhd = Util.getIntegerCodeForString("tfhd"); + public static final int TYPE_tfhd = 0x74666864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_trex = Util.getIntegerCodeForString("trex"); + public static final int TYPE_trex = 0x74726578; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_trun = Util.getIntegerCodeForString("trun"); + public static final int TYPE_trun = 0x7472756e; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sidx = Util.getIntegerCodeForString("sidx"); + public static final int TYPE_sidx = 0x73696478; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_moov = Util.getIntegerCodeForString("moov"); + public static final int TYPE_moov = 0x6d6f6f76; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mvhd = Util.getIntegerCodeForString("mvhd"); + public static final int TYPE_mvhd = 0x6d766864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_trak = Util.getIntegerCodeForString("trak"); + public static final int TYPE_trak = 0x7472616b; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mdia = Util.getIntegerCodeForString("mdia"); + public static final int TYPE_mdia = 0x6d646961; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_minf = Util.getIntegerCodeForString("minf"); + public static final int TYPE_minf = 0x6d696e66; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stbl = Util.getIntegerCodeForString("stbl"); + public static final int TYPE_stbl = 0x7374626c; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_esds = Util.getIntegerCodeForString("esds"); + public static final int TYPE_esds = 0x65736473; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_moof = Util.getIntegerCodeForString("moof"); + public static final int TYPE_moof = 0x6d6f6f66; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_traf = Util.getIntegerCodeForString("traf"); + public static final int TYPE_traf = 0x74726166; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mvex = Util.getIntegerCodeForString("mvex"); + public static final int TYPE_mvex = 0x6d766578; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mehd = Util.getIntegerCodeForString("mehd"); + public static final int TYPE_mehd = 0x6d656864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_tkhd = Util.getIntegerCodeForString("tkhd"); + public static final int TYPE_tkhd = 0x746b6864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_edts = Util.getIntegerCodeForString("edts"); + public static final int TYPE_edts = 0x65647473; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_elst = Util.getIntegerCodeForString("elst"); + public static final int TYPE_elst = 0x656c7374; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mdhd = Util.getIntegerCodeForString("mdhd"); + public static final int TYPE_mdhd = 0x6d646864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_hdlr = Util.getIntegerCodeForString("hdlr"); + public static final int TYPE_hdlr = 0x68646c72; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stsd = Util.getIntegerCodeForString("stsd"); + public static final int TYPE_stsd = 0x73747364; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_pssh = Util.getIntegerCodeForString("pssh"); + public static final int TYPE_pssh = 0x70737368; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sinf = Util.getIntegerCodeForString("sinf"); + public static final int TYPE_sinf = 0x73696e66; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_schm = Util.getIntegerCodeForString("schm"); + public static final int TYPE_schm = 0x7363686d; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_schi = Util.getIntegerCodeForString("schi"); + public static final int TYPE_schi = 0x73636869; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_tenc = Util.getIntegerCodeForString("tenc"); + public static final int TYPE_tenc = 0x74656e63; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_encv = Util.getIntegerCodeForString("encv"); + public static final int TYPE_encv = 0x656e6376; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_enca = Util.getIntegerCodeForString("enca"); + public static final int TYPE_enca = 0x656e6361; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_frma = Util.getIntegerCodeForString("frma"); + public static final int TYPE_frma = 0x66726d61; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_saiz = Util.getIntegerCodeForString("saiz"); + public static final int TYPE_saiz = 0x7361697a; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_saio = Util.getIntegerCodeForString("saio"); + public static final int TYPE_saio = 0x7361696f; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sbgp = Util.getIntegerCodeForString("sbgp"); + public static final int TYPE_sbgp = 0x73626770; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sgpd = Util.getIntegerCodeForString("sgpd"); + public static final int TYPE_sgpd = 0x73677064; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_uuid = Util.getIntegerCodeForString("uuid"); + public static final int TYPE_uuid = 0x75756964; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_senc = Util.getIntegerCodeForString("senc"); + public static final int TYPE_senc = 0x73656e63; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_pasp = Util.getIntegerCodeForString("pasp"); + public static final int TYPE_pasp = 0x70617370; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_TTML = Util.getIntegerCodeForString("TTML"); + public static final int TYPE_TTML = 0x54544d4c; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_vmhd = Util.getIntegerCodeForString("vmhd"); + public static final int TYPE_vmhd = 0x766d6864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mp4v = Util.getIntegerCodeForString("mp4v"); + public static final int TYPE_mp4v = 0x6d703476; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stts = Util.getIntegerCodeForString("stts"); + public static final int TYPE_stts = 0x73747473; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stss = Util.getIntegerCodeForString("stss"); + public static final int TYPE_stss = 0x73747373; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ctts = Util.getIntegerCodeForString("ctts"); + public static final int TYPE_ctts = 0x63747473; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stsc = Util.getIntegerCodeForString("stsc"); + public static final int TYPE_stsc = 0x73747363; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stsz = Util.getIntegerCodeForString("stsz"); + public static final int TYPE_stsz = 0x7374737a; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stz2 = Util.getIntegerCodeForString("stz2"); + public static final int TYPE_stz2 = 0x73747a32; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stco = Util.getIntegerCodeForString("stco"); + public static final int TYPE_stco = 0x7374636f; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_co64 = Util.getIntegerCodeForString("co64"); + public static final int TYPE_co64 = 0x636f3634; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_tx3g = Util.getIntegerCodeForString("tx3g"); + public static final int TYPE_tx3g = 0x74783367; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_wvtt = Util.getIntegerCodeForString("wvtt"); + public static final int TYPE_wvtt = 0x77767474; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stpp = Util.getIntegerCodeForString("stpp"); + public static final int TYPE_stpp = 0x73747070; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_c608 = Util.getIntegerCodeForString("c608"); + public static final int TYPE_c608 = 0x63363038; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_samr = Util.getIntegerCodeForString("samr"); + public static final int TYPE_samr = 0x73616d72; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb"); + public static final int TYPE_sawb = 0x73617762; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_udta = Util.getIntegerCodeForString("udta"); + public static final int TYPE_udta = 0x75647461; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_meta = Util.getIntegerCodeForString("meta"); + public static final int TYPE_meta = 0x6d657461; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_keys = Util.getIntegerCodeForString("keys"); + public static final int TYPE_keys = 0x6b657973; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ilst = Util.getIntegerCodeForString("ilst"); + public static final int TYPE_ilst = 0x696c7374; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mean = Util.getIntegerCodeForString("mean"); + public static final int TYPE_mean = 0x6d65616e; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_name = Util.getIntegerCodeForString("name"); + public static final int TYPE_name = 0x6e616d65; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_data = Util.getIntegerCodeForString("data"); + public static final int TYPE_data = 0x64617461; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_emsg = Util.getIntegerCodeForString("emsg"); + public static final int TYPE_emsg = 0x656d7367; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_st3d = Util.getIntegerCodeForString("st3d"); + public static final int TYPE_st3d = 0x73743364; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sv3d = Util.getIntegerCodeForString("sv3d"); + public static final int TYPE_sv3d = 0x73763364; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_proj = Util.getIntegerCodeForString("proj"); + public static final int TYPE_proj = 0x70726f6a; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_camm = Util.getIntegerCodeForString("camm"); + public static final int TYPE_camm = 0x63616d6d; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_alac = Util.getIntegerCodeForString("alac"); + public static final int TYPE_alac = 0x616c6163; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_alaw = Util.getIntegerCodeForString("alaw"); + public static final int TYPE_alaw = 0x616c6177; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ulaw = Util.getIntegerCodeForString("ulaw"); + public static final int TYPE_ulaw = 0x756c6177; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_Opus = Util.getIntegerCodeForString("Opus"); + public static final int TYPE_Opus = 0x4f707573; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dOps = Util.getIntegerCodeForString("dOps"); + public static final int TYPE_dOps = 0x644f7073; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_fLaC = Util.getIntegerCodeForString("fLaC"); + public static final int TYPE_fLaC = 0x664c6143; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dfLa = Util.getIntegerCodeForString("dfLa"); + public static final int TYPE_dfLa = 0x64664c61; public final int type; 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 90f8b125ac..ea45374f86 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 @@ -48,28 +48,28 @@ import java.util.List; private static final String TAG = "AtomParsers"; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_vide = Util.getIntegerCodeForString("vide"); + private static final int TYPE_vide = 0x76696465; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_soun = Util.getIntegerCodeForString("soun"); + private static final int TYPE_soun = 0x736f756e; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_text = Util.getIntegerCodeForString("text"); + private static final int TYPE_text = 0x74657874; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl"); + private static final int TYPE_sbtl = 0x7362746c; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_subt = Util.getIntegerCodeForString("subt"); + private static final int TYPE_subt = 0x73756274; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp"); + private static final int TYPE_clcp = 0x636c6370; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_meta = Util.getIntegerCodeForString("meta"); + private static final int TYPE_meta = 0x6d657461; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_mdta = Util.getIntegerCodeForString("mdta"); + private static final int TYPE_mdta = 0x6d647461; /** * The threshold number of samples to trim from the start/end of an audio track when applying an 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 4d51fb9b8e..e0673dd4fa 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 @@ -106,7 +106,7 @@ public class FragmentedMp4Extractor implements Extractor { private static final String TAG = "FragmentedMp4Extractor"; @SuppressWarnings("ConstantCaseForConstants") - private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); + private static final int SAMPLE_GROUP_TYPE_seig = 0x73656967; private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12}; 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 e9c9f7faf5..bec2cdbb5f 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 @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.metadata.id3.InternalFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; /** Utilities for handling metadata in MP4. */ @@ -36,41 +35,41 @@ import java.nio.ByteBuffer; private static final String TAG = "MetadataUtil"; // Codes that start with the copyright character (omitted) and have equivalent ID3 frames. - private static final int SHORT_TYPE_NAME_1 = Util.getIntegerCodeForString("nam"); - private static final int SHORT_TYPE_NAME_2 = Util.getIntegerCodeForString("trk"); - private static final int SHORT_TYPE_COMMENT = Util.getIntegerCodeForString("cmt"); - private static final int SHORT_TYPE_YEAR = Util.getIntegerCodeForString("day"); - private static final int SHORT_TYPE_ARTIST = Util.getIntegerCodeForString("ART"); - private static final int SHORT_TYPE_ENCODER = Util.getIntegerCodeForString("too"); - private static final int SHORT_TYPE_ALBUM = Util.getIntegerCodeForString("alb"); - private static final int SHORT_TYPE_COMPOSER_1 = Util.getIntegerCodeForString("com"); - private static final int SHORT_TYPE_COMPOSER_2 = Util.getIntegerCodeForString("wrt"); - private static final int SHORT_TYPE_LYRICS = Util.getIntegerCodeForString("lyr"); - private static final int SHORT_TYPE_GENRE = Util.getIntegerCodeForString("gen"); + private static final int SHORT_TYPE_NAME_1 = 0x006e616d; + private static final int SHORT_TYPE_NAME_2 = 0x0074726b; + private static final int SHORT_TYPE_COMMENT = 0x00636d74; + private static final int SHORT_TYPE_YEAR = 0x00646179; + private static final int SHORT_TYPE_ARTIST = 0x00415254; + private static final int SHORT_TYPE_ENCODER = 0x00746f6f; + private static final int SHORT_TYPE_ALBUM = 0x00616c62; + private static final int SHORT_TYPE_COMPOSER_1 = 0x00636f6d; + private static final int SHORT_TYPE_COMPOSER_2 = 0x00777274; + private static final int SHORT_TYPE_LYRICS = 0x006c7972; + private static final int SHORT_TYPE_GENRE = 0x0067656e; // Codes that have equivalent ID3 frames. - private static final int TYPE_COVER_ART = Util.getIntegerCodeForString("covr"); - private static final int TYPE_GENRE = Util.getIntegerCodeForString("gnre"); - private static final int TYPE_GROUPING = Util.getIntegerCodeForString("grp"); - private static final int TYPE_DISK_NUMBER = Util.getIntegerCodeForString("disk"); - private static final int TYPE_TRACK_NUMBER = Util.getIntegerCodeForString("trkn"); - private static final int TYPE_TEMPO = Util.getIntegerCodeForString("tmpo"); - private static final int TYPE_COMPILATION = Util.getIntegerCodeForString("cpil"); - private static final int TYPE_ALBUM_ARTIST = Util.getIntegerCodeForString("aART"); - private static final int TYPE_SORT_TRACK_NAME = Util.getIntegerCodeForString("sonm"); - private static final int TYPE_SORT_ALBUM = Util.getIntegerCodeForString("soal"); - private static final int TYPE_SORT_ARTIST = Util.getIntegerCodeForString("soar"); - private static final int TYPE_SORT_ALBUM_ARTIST = Util.getIntegerCodeForString("soaa"); - private static final int TYPE_SORT_COMPOSER = Util.getIntegerCodeForString("soco"); + private static final int TYPE_COVER_ART = 0x636f7672; + private static final int TYPE_GENRE = 0x676e7265; + private static final int TYPE_GROUPING = 0x00677270; + private static final int TYPE_DISK_NUMBER = 0x6469736b; + private static final int TYPE_TRACK_NUMBER = 0x74726b6e; + private static final int TYPE_TEMPO = 0x746d706f; + private static final int TYPE_COMPILATION = 0x6370696c; + private static final int TYPE_ALBUM_ARTIST = 0x61415254; + private static final int TYPE_SORT_TRACK_NAME = 0x736f6e6d; + private static final int TYPE_SORT_ALBUM = 0x736f616c; + private static final int TYPE_SORT_ARTIST = 0x736f6172; + private static final int TYPE_SORT_ALBUM_ARTIST = 0x736f6161; + private static final int TYPE_SORT_COMPOSER = 0x736f636f; // Types that do not have equivalent ID3 frames. - private static final int TYPE_RATING = Util.getIntegerCodeForString("rtng"); - private static final int TYPE_GAPLESS_ALBUM = Util.getIntegerCodeForString("pgap"); - private static final int TYPE_TV_SORT_SHOW = Util.getIntegerCodeForString("sosn"); - private static final int TYPE_TV_SHOW = Util.getIntegerCodeForString("tvsh"); + private static final int TYPE_RATING = 0x72746e67; + private static final int TYPE_GAPLESS_ALBUM = 0x70676170; + private static final int TYPE_TV_SORT_SHOW = 0x736f736e; + private static final int TYPE_TV_SHOW = 0x74767368; // Type for items that are intended for internal use by the player. - private static final int TYPE_INTERNAL = Util.getIntegerCodeForString("----"); + private static final int TYPE_INTERNAL = 0x2d2d2d2d; private static final int PICTURE_TYPE_FRONT_COVER = 3; 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 75966bff66..16f5b1fb29 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 @@ -35,7 +35,6 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -78,7 +77,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { private static final int STATE_READING_SAMPLE = 2; /** Brand stored in the ftyp atom for QuickTime media. */ - private static final int BRAND_QUICKTIME = Util.getIntegerCodeForString("qt "); + private static final int BRAND_QUICKTIME = 0x71742020; /** * When seeking within the source, if the offset is greater than or equal to this value (or the diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java index 5c5afe39a8..95193785c0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.extractor.mp4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** @@ -32,32 +31,32 @@ import java.io.IOException; private static final int[] COMPATIBLE_BRANDS = new int[] { - Util.getIntegerCodeForString("isom"), - Util.getIntegerCodeForString("iso2"), - Util.getIntegerCodeForString("iso3"), - Util.getIntegerCodeForString("iso4"), - Util.getIntegerCodeForString("iso5"), - Util.getIntegerCodeForString("iso6"), - Util.getIntegerCodeForString("avc1"), - Util.getIntegerCodeForString("hvc1"), - Util.getIntegerCodeForString("hev1"), - Util.getIntegerCodeForString("av01"), - Util.getIntegerCodeForString("mp41"), - Util.getIntegerCodeForString("mp42"), - Util.getIntegerCodeForString("3g2a"), - Util.getIntegerCodeForString("3g2b"), - Util.getIntegerCodeForString("3gr6"), - Util.getIntegerCodeForString("3gs6"), - Util.getIntegerCodeForString("3ge6"), - Util.getIntegerCodeForString("3gg6"), - Util.getIntegerCodeForString("M4V "), - Util.getIntegerCodeForString("M4A "), - Util.getIntegerCodeForString("f4v "), - Util.getIntegerCodeForString("kddi"), - Util.getIntegerCodeForString("M4VP"), - Util.getIntegerCodeForString("qt "), // Apple QuickTime - Util.getIntegerCodeForString("MSNV"), // Sony PSP - Util.getIntegerCodeForString("dby1"), // Dolby Vision + 0x69736f6d, // isom + 0x69736f32, // iso2 + 0x69736f33, // iso3 + 0x69736f34, // iso4 + 0x69736f35, // iso5 + 0x69736f36, // iso6 + 0x61766331, // avc1 + 0x68766331, // hvc1 + 0x68657631, // hev1 + 0x61763031, // av01 + 0x6d703431, // mp41 + 0x6d703432, // mp42 + 0x33673261, // 3g2a + 0x33673262, // 3g2b + 0x33677236, // 3gr6 + 0x33677336, // 3gs6 + 0x33676536, // 3ge6 + 0x33676736, // 3gg6 + 0x4d345620, // M4V[space] + 0x4d344120, // M4A[space] + 0x66347620, // f4v[space] + 0x6b646469, // kddi + 0x4d345650, // M4VP + 0x71742020, // qt[space][space], Apple QuickTime + 0x4d534e56, // MSNV, Sony PSP + 0x64627931, // dby1, Dolby Vision }; /** @@ -188,7 +187,7 @@ import java.io.IOException; */ private static boolean isCompatibleBrand(int brand) { // Accept all brands starting '3gp'. - if (brand >>> 8 == Util.getIntegerCodeForString("3gp")) { + if (brand >>> 8 == 0x00336770) { return true; } for (int compatibleBrand : COMPATIBLE_BRANDS) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java index bbf7e2fc6b..ff32ae3462 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java @@ -19,7 +19,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; @@ -34,7 +33,7 @@ import java.io.IOException; public static final int MAX_PAGE_SIZE = EMPTY_PAGE_HEADER_SIZE + MAX_SEGMENT_COUNT + MAX_PAGE_PAYLOAD; - private static final int TYPE_OGGS = Util.getIntegerCodeForString("OggS"); + private static final int TYPE_OGGS = 0x4f676753; public int revision; public int type; 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 ff5f115573..90ae3f0f47 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 @@ -19,7 +19,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; @@ -38,7 +37,7 @@ import java.util.List; */ private static final int SAMPLE_RATE = 48000; - private static final int OPUS_CODE = Util.getIntegerCodeForString("Opus"); + private static final int OPUS_CODE = 0x4f707573; private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'}; private boolean headerRead; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java index aa77aba30e..3d76276240 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java @@ -25,7 +25,6 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** @@ -35,7 +34,7 @@ public final class RawCcExtractor implements Extractor { private static final int SCRATCH_SIZE = 9; private static final int HEADER_SIZE = 8; - private static final int HEADER_ID = Util.getIntegerCodeForString("RCC\u0001"); + private static final int HEADER_ID = 0x52434301; private static final int TIMESTAMP_SIZE_V0 = 4; private static final int TIMESTAMP_SIZE_V1 = 8; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index 889a49755a..0a0755327c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** @@ -45,7 +44,7 @@ public final class Ac3Extractor implements Extractor { private static final int MAX_SNIFF_BYTES = 8 * 1024; private static final int AC3_SYNC_WORD = 0x0B77; private static final int MAX_SYNC_FRAME_SIZE = 2786; - private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); + private static final int ID3_TAG = 0x00494433; private final long firstSampleTimestampUs; private final Ac3Reader reader; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java index 133c0f368b..4db02e0d83 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java @@ -29,7 +29,6 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** Extracts data from AC-4 bitstreams. */ @@ -53,7 +52,7 @@ public final class Ac4Extractor implements Extractor { /** The size of the frame header, in bytes. */ private static final int FRAME_HEADER_SIZE = 7; - private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); + private static final int ID3_TAG = 0x00494433; private final long firstSampleTimestampUs; private final Ac4Reader reader; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index 9526a65766..a636d2f680 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -32,7 +32,6 @@ import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerat import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -66,7 +65,7 @@ public final class AdtsExtractor implements Extractor { public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; private static final int MAX_PACKET_SIZE = 2 * 1024; - private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); + private static final int ID3_TAG = 0x00494433; /** * The maximum number of bytes to search when sniffing, excluding the header, before giving up. * Frame sizes are represented by 13-bit fields, so expect a valid frame in the first 8192 bytes. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index a2f8568cbb..d198e816d5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -101,10 +101,10 @@ public final class TsExtractor implements Extractor { private static final int TS_PAT_PID = 0; private static final int MAX_PID_PLUS_ONE = 0x2000; - private static final long AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("AC-3"); - private static final long E_AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("EAC3"); - private static final long AC4_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("AC-4"); - private static final long HEVC_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("HEVC"); + private static final long AC3_FORMAT_IDENTIFIER = 0x41432d33; + private static final long E_AC3_FORMAT_IDENTIFIER = 0x45414333; + private static final long AC4_FORMAT_IDENTIFIER = 0x41432d34; + private static final long HEVC_FORMAT_IDENTIFIER = 0x48455643; private static final int BUFFER_SIZE = TS_PACKET_SIZE * 50; private static final int SNIFF_TS_PACKET_COUNT = 5; 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 c7b7a40ead..7a6a7e346f 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 @@ -22,7 +22,6 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */ @@ -122,11 +121,13 @@ import java.io.IOException; ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); // Skip all chunks until we hit the data header. ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); - while (chunkHeader.id != Util.getIntegerCodeForString("data")) { + final int data = 0x64617461; + while (chunkHeader.id != data) { Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size; // Override size of RIFF chunk, since it describes its size as the entire file. - if (chunkHeader.id == Util.getIntegerCodeForString("RIFF")) { + final int riff = 0x52494646; + if (chunkHeader.id == riff) { bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4; } if (bytesToSkip > Integer.MAX_VALUE) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index fff0828b3a..4417126427 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -61,10 +61,8 @@ public final class Id3Decoder implements MetadataDecoder { private static final String TAG = "Id3Decoder"; - /** - * The first three bytes of a well formed ID3 tag header. - */ - public static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); + /** The first three bytes of a well formed ID3 tag header. */ + public static final int ID3_TAG = 0x00494433; /** * Length of an ID3 tag header. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java index 75fe8fed25..cdc545e459 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java @@ -19,14 +19,13 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; /** Utility methods for handling CEA-608/708 messages. Defined in A/53 Part 4:2009. */ public final class CeaUtil { private static final String TAG = "CeaUtil"; - public static final int USER_DATA_IDENTIFIER_GA94 = Util.getIntegerCodeForString("GA94"); + public static final int USER_DATA_IDENTIFIER_GA94 = 0x47413934; public static final int USER_DATA_TYPE_CODE_MPEG_CC = 0x3; private static final int PAYLOAD_TYPE_CC = 4; 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 9211dc51ce..89017a40c0 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 @@ -43,8 +43,8 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { private static final char BOM_UTF16_BE = '\uFEFF'; private static final char BOM_UTF16_LE = '\uFFFE'; - private static final int TYPE_STYL = Util.getIntegerCodeForString("styl"); - private static final int TYPE_TBOX = Util.getIntegerCodeForString("tbox"); + private static final int TYPE_STYL = 0x7374796c; + private static final int TYPE_TBOX = 0x74626f78; private static final String TX3G_SERIF = "Serif"; private static final int SIZE_ATOM_HEADER = 8; 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 5e425cc12f..b977f61a8a 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 @@ -31,13 +31,13 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { private static final int BOX_HEADER_SIZE = 8; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_payl = Util.getIntegerCodeForString("payl"); + private static final int TYPE_payl = 0x7061796c; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_sttg = Util.getIntegerCodeForString("sttg"); + private static final int TYPE_sttg = 0x73747467; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_vttc = Util.getIntegerCodeForString("vttc"); + private static final int TYPE_vttc = 0x76747463; private final ParsableByteArray sampleData; private final WebvttCue.Builder builder; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java index 527aa5db4f..eadc617ea7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java @@ -36,12 +36,12 @@ import java.util.zip.Inflater; */ public final class ProjectionDecoder { - private static final int TYPE_YTMP = Util.getIntegerCodeForString("ytmp"); - private static final int TYPE_MSHP = Util.getIntegerCodeForString("mshp"); - private static final int TYPE_RAW = Util.getIntegerCodeForString("raw "); - private static final int TYPE_DFL8 = Util.getIntegerCodeForString("dfl8"); - private static final int TYPE_MESH = Util.getIntegerCodeForString("mesh"); - private static final int TYPE_PROJ = Util.getIntegerCodeForString("proj"); + private static final int TYPE_YTMP = 0x79746d70; + private static final int TYPE_MSHP = 0x6d736870; + private static final int TYPE_RAW = 0x72617720; + private static final int TYPE_DFL8 = 0x64666c38; + private static final int TYPE_MESH = 0x6d657368; + private static final int TYPE_PROJ = 0x70726f6a; // Sanity limits to prevent a bad file from creating an OOM situation. We don't expect a mesh to // exceed these limits. From 6abd5dc66f93b088693e351e4514fea296291924 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 22 May 2019 09:17:49 +0100 Subject: [PATCH 1299/1556] Add missing annotations dependency Issue: #5926 PiperOrigin-RevId: 249404152 --- extensions/ima/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index a91bbbd981..2df9448d08 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -34,6 +34,7 @@ android { dependencies { api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2' implementation project(modulePrefix + 'library-core') + implementation 'androidx.annotation:annotation:1.0.2' implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0' testImplementation project(modulePrefix + 'testutils-robolectric') } From a4d18a7457c9e5cdd7528189666c7ca8ca4d45f9 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 22 May 2019 11:27:57 +0100 Subject: [PATCH 1300/1556] Remove mistakenly left link in vp9 readme PiperOrigin-RevId: 249417898 --- extensions/vp9/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 0de29eea32..2c5b64f8bd 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -66,7 +66,6 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4 [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html -[#3520]: https://github.com/google/ExoPlayer/issues/3520 ## Notes ## From d836957138ba56db4d6459dbe87210ce24d97166 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 May 2019 11:43:37 +0100 Subject: [PATCH 1301/1556] Remove some DataSource implementations from nullness blacklist PiperOrigin-RevId: 249419193 --- .../exoplayer2/upstream/AssetDataSource.java | 14 +++++---- .../upstream/ByteArrayDataSink.java | 15 ++++++---- .../upstream/ByteArrayDataSource.java | 9 +++--- .../upstream/ContentDataSource.java | 24 ++++++++++----- .../upstream/DataSchemeDataSource.java | 18 ++++++++---- .../exoplayer2/upstream/DummyDataSource.java | 7 +++-- .../exoplayer2/upstream/FileDataSource.java | 21 ++++++++++---- .../upstream/RawResourceDataSource.java | 29 +++++++++++++------ .../exoplayer2/upstream/UdpDataSource.java | 13 +++++---- .../upstream/crypto/AesCipherDataSink.java | 19 +++++++----- .../upstream/crypto/AesCipherDataSource.java | 6 ++-- .../upstream/crypto/CryptoUtil.java | 8 +++-- 12 files changed, 119 insertions(+), 64 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java index eeb0f1c957..3c92b039cc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java @@ -15,11 +15,14 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.content.Context; import android.content.res.AssetManager; import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; @@ -40,8 +43,8 @@ public final class AssetDataSource extends BaseDataSource { private final AssetManager assetManager; - private @Nullable Uri uri; - private @Nullable InputStream inputStream; + @Nullable private Uri uri; + @Nullable private InputStream inputStream; private long bytesRemaining; private boolean opened; @@ -55,7 +58,7 @@ public final class AssetDataSource extends BaseDataSource { public long open(DataSpec dataSpec) throws AssetDataSourceException { try { uri = dataSpec.uri; - String path = uri.getPath(); + String path = Assertions.checkNotNull(uri.getPath()); if (path.startsWith("/android_asset/")) { path = path.substring(15); } else if (path.startsWith("/")) { @@ -101,7 +104,7 @@ public final class AssetDataSource extends BaseDataSource { try { int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength : (int) Math.min(bytesRemaining, readLength); - bytesRead = inputStream.read(buffer, offset, bytesToRead); + bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead); } catch (IOException e) { throw new AssetDataSourceException(e); } @@ -121,7 +124,8 @@ public final class AssetDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java index 4017c1f028..a9f9da0a95 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java @@ -15,20 +15,24 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.io.ByteArrayOutputStream; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link DataSink} for writing to a byte array. */ public final class ByteArrayDataSink implements DataSink { - private ByteArrayOutputStream stream; + @MonotonicNonNull private ByteArrayOutputStream stream; @Override - public void open(DataSpec dataSpec) throws IOException { + public void open(DataSpec dataSpec) { if (dataSpec.length == C.LENGTH_UNSET) { stream = new ByteArrayOutputStream(); } else { @@ -39,18 +43,19 @@ public final class ByteArrayDataSink implements DataSink { @Override public void close() throws IOException { - stream.close(); + castNonNull(stream).close(); } @Override - public void write(byte[] buffer, int offset, int length) throws IOException { - stream.write(buffer, offset, length); + public void write(byte[] buffer, int offset, int length) { + castNonNull(stream).write(buffer, offset, length); } /** * Returns the data written to the sink since the last call to {@link #open(DataSpec)}, or null if * {@link #open(DataSpec)} has never been called. */ + @Nullable public byte[] getData() { return stream == null ? null : stream.toByteArray(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java index c450896676..ed5ba9064b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java @@ -26,7 +26,7 @@ public final class ByteArrayDataSource extends BaseDataSource { private final byte[] data; - private @Nullable Uri uri; + @Nullable private Uri uri; private int readPosition; private int bytesRemaining; private boolean opened; @@ -58,7 +58,7 @@ public final class ByteArrayDataSource extends BaseDataSource { } @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { + public int read(byte[] buffer, int offset, int readLength) { if (readLength == 0) { return 0; } else if (bytesRemaining == 0) { @@ -74,12 +74,13 @@ public final class ByteArrayDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } @Override - public void close() throws IOException { + public void close() { if (opened) { opened = false; transferEnded(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index 8df69ffb7a..baaa677127 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; @@ -43,9 +45,9 @@ public final class ContentDataSource extends BaseDataSource { private final ContentResolver resolver; - private @Nullable Uri uri; - private @Nullable AssetFileDescriptor assetFileDescriptor; - private @Nullable FileInputStream inputStream; + @Nullable private Uri uri; + @Nullable private AssetFileDescriptor assetFileDescriptor; + @Nullable private FileInputStream inputStream; private long bytesRemaining; private boolean opened; @@ -60,13 +62,18 @@ public final class ContentDataSource extends BaseDataSource { @Override public long open(DataSpec dataSpec) throws ContentDataSourceException { try { - uri = dataSpec.uri; + Uri uri = dataSpec.uri; + this.uri = uri; + transferInitializing(dataSpec); - assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r"); + AssetFileDescriptor assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r"); + this.assetFileDescriptor = assetFileDescriptor; if (assetFileDescriptor == null) { throw new FileNotFoundException("Could not open file descriptor for: " + uri); } - inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + this.inputStream = inputStream; + long assetStartOffset = assetFileDescriptor.getStartOffset(); long skipped = inputStream.skip(assetStartOffset + dataSpec.position) - assetStartOffset; if (skipped != dataSpec.position) { @@ -110,7 +117,7 @@ public final class ContentDataSource extends BaseDataSource { try { int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength : (int) Math.min(bytesRemaining, readLength); - bytesRead = inputStream.read(buffer, offset, bytesToRead); + bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead); } catch (IOException e) { throw new ContentDataSourceException(e); } @@ -130,7 +137,8 @@ public final class ContentDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } 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 de4a75d607..03804fa577 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 @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.net.Uri; import androidx.annotation.Nullable; import android.util.Base64; @@ -29,9 +31,10 @@ public final class DataSchemeDataSource extends BaseDataSource { public static final String SCHEME_DATA = "data"; - private @Nullable DataSpec dataSpec; + @Nullable private DataSpec dataSpec; + @Nullable private byte[] data; + private int dataLength; private int bytesRead; - private @Nullable byte[] data; public DataSchemeDataSource() { super(/* isNetwork= */ false); @@ -54,15 +57,17 @@ public final class DataSchemeDataSource extends BaseDataSource { if (uriParts[0].contains(";base64")) { try { data = Base64.decode(dataString, 0); + dataLength = data.length; } catch (IllegalArgumentException e) { throw new ParserException("Error while parsing Base64 encoded string: " + dataString, e); } } else { // TODO: Add support for other charsets. data = Util.getUtf8Bytes(URLDecoder.decode(dataString, C.ASCII_NAME)); + dataLength = data.length; } transferStarted(dataSpec); - return data.length; + return dataLength; } @Override @@ -70,19 +75,20 @@ public final class DataSchemeDataSource extends BaseDataSource { if (readLength == 0) { return 0; } - int remainingBytes = data.length - bytesRead; + int remainingBytes = dataLength - bytesRead; if (remainingBytes == 0) { return C.RESULT_END_OF_INPUT; } readLength = Math.min(readLength, remainingBytes); - System.arraycopy(data, bytesRead, buffer, offset, readLength); + System.arraycopy(castNonNull(data), bytesRead, buffer, offset, readLength); bytesRead += readLength; bytesTransferred(readLength); return readLength; } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return dataSpec != null ? dataSpec.uri : null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java index 026bc0b9c7..4124a2531f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java @@ -42,17 +42,18 @@ public final class DummyDataSource implements DataSource { } @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { + public int read(byte[] buffer, int offset, int readLength) { throw new UnsupportedOperationException(); } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return null; } @Override - public void close() throws IOException { + public void close() { // do nothing. } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java index cead366360..e329dc722e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java @@ -15,9 +15,12 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; import java.io.IOException; import java.io.RandomAccessFile; @@ -36,8 +39,8 @@ public final class FileDataSource extends BaseDataSource { } - private @Nullable RandomAccessFile file; - private @Nullable Uri uri; + @Nullable private RandomAccessFile file; + @Nullable private Uri uri; private long bytesRemaining; private boolean opened; @@ -48,9 +51,13 @@ public final class FileDataSource extends BaseDataSource { @Override public long open(DataSpec dataSpec) throws FileDataSourceException { try { - uri = dataSpec.uri; + Uri uri = dataSpec.uri; + this.uri = uri; + transferInitializing(dataSpec); - file = new RandomAccessFile(dataSpec.uri.getPath(), "r"); + RandomAccessFile file = new RandomAccessFile(Assertions.checkNotNull(uri.getPath()), "r"); + this.file = file; + file.seek(dataSpec.position); bytesRemaining = dataSpec.length == C.LENGTH_UNSET ? file.length() - dataSpec.position : dataSpec.length; @@ -76,7 +83,8 @@ public final class FileDataSource extends BaseDataSource { } else { int bytesRead; try { - bytesRead = file.read(buffer, offset, (int) Math.min(bytesRemaining, readLength)); + bytesRead = + castNonNull(file).read(buffer, offset, (int) Math.min(bytesRemaining, readLength)); } catch (IOException e) { throw new FileDataSourceException(e); } @@ -91,7 +99,8 @@ public final class FileDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java index 7b70bcc5c4..ff032a4ed0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.content.Context; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; @@ -22,6 +24,7 @@ import android.net.Uri; import androidx.annotation.Nullable; import android.text.TextUtils; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; import java.io.FileInputStream; import java.io.IOException; @@ -64,9 +67,9 @@ public final class RawResourceDataSource extends BaseDataSource { private final Resources resources; - private @Nullable Uri uri; - private @Nullable AssetFileDescriptor assetFileDescriptor; - private @Nullable InputStream inputStream; + @Nullable private Uri uri; + @Nullable private AssetFileDescriptor assetFileDescriptor; + @Nullable private InputStream inputStream; private long bytesRemaining; private boolean opened; @@ -81,21 +84,28 @@ public final class RawResourceDataSource extends BaseDataSource { @Override public long open(DataSpec dataSpec) throws RawResourceDataSourceException { try { - uri = dataSpec.uri; + Uri uri = dataSpec.uri; + this.uri = uri; if (!TextUtils.equals(RAW_RESOURCE_SCHEME, uri.getScheme())) { throw new RawResourceDataSourceException("URI must use scheme " + RAW_RESOURCE_SCHEME); } int resourceId; try { - resourceId = Integer.parseInt(uri.getLastPathSegment()); + resourceId = Integer.parseInt(Assertions.checkNotNull(uri.getLastPathSegment())); } catch (NumberFormatException e) { throw new RawResourceDataSourceException("Resource identifier must be an integer."); } transferInitializing(dataSpec); - assetFileDescriptor = resources.openRawResourceFd(resourceId); - inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + AssetFileDescriptor assetFileDescriptor = resources.openRawResourceFd(resourceId); + this.assetFileDescriptor = assetFileDescriptor; + if (assetFileDescriptor == null) { + throw new RawResourceDataSourceException("Resource is compressed: " + uri); + } + FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + this.inputStream = inputStream; + inputStream.skip(assetFileDescriptor.getStartOffset()); long skipped = inputStream.skip(dataSpec.position); if (skipped < dataSpec.position) { @@ -133,7 +143,7 @@ public final class RawResourceDataSource extends BaseDataSource { try { int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength : (int) Math.min(bytesRemaining, readLength); - bytesRead = inputStream.read(buffer, offset, bytesToRead); + bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead); } catch (IOException e) { throw new RawResourceDataSourceException(e); } @@ -153,7 +163,8 @@ public final class RawResourceDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java index fcfeef3fb4..4d9b375334 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java @@ -52,11 +52,11 @@ public final class UdpDataSource extends BaseDataSource { private final byte[] packetBuffer; private final DatagramPacket packet; - private @Nullable Uri uri; - private @Nullable DatagramSocket socket; - private @Nullable MulticastSocket multicastSocket; - private @Nullable InetAddress address; - private @Nullable InetSocketAddress socketAddress; + @Nullable private Uri uri; + @Nullable private DatagramSocket socket; + @Nullable private MulticastSocket multicastSocket; + @Nullable private InetAddress address; + @Nullable private InetSocketAddress socketAddress; private boolean opened; private int packetRemaining; @@ -144,7 +144,8 @@ public final class UdpDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java index ccf9a5b3f5..522fdc9a3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.upstream.crypto; +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import androidx.annotation.Nullable; import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSpec; import java.io.IOException; @@ -27,9 +30,9 @@ public final class AesCipherDataSink implements DataSink { private final DataSink wrappedDataSink; private final byte[] secretKey; - private final byte[] scratch; + @Nullable private final byte[] scratch; - private AesFlushingCipher cipher; + @Nullable private AesFlushingCipher cipher; /** * Create an instance whose {@code write} methods have the side effect of overwriting the input @@ -52,9 +55,10 @@ public final class AesCipherDataSink implements DataSink { * @param scratch Scratch space. Data is decrypted into this array before being written to the * wrapped {@link DataSink}. It should be of appropriate size for the expected writes. If a * write is larger than the size of this array the write will still succeed, but multiple - * cipher calls will be required to complete the operation. + * cipher calls will be required to complete the operation. If {@code null} then decryption + * will overwrite the input {@code data}. */ - public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, byte[] scratch) { + public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, @Nullable byte[] scratch) { this.wrappedDataSink = wrappedDataSink; this.secretKey = secretKey; this.scratch = scratch; @@ -72,15 +76,16 @@ public final class AesCipherDataSink implements DataSink { public void write(byte[] data, int offset, int length) throws IOException { if (scratch == null) { // In-place mode. Writes over the input data. - cipher.updateInPlace(data, offset, length); + castNonNull(cipher).updateInPlace(data, offset, length); wrappedDataSink.write(data, offset, length); } else { // Use scratch space. The original data remains intact. int bytesProcessed = 0; while (bytesProcessed < length) { int bytesToProcess = Math.min(length - bytesProcessed, scratch.length); - cipher.update(data, offset + bytesProcessed, bytesToProcess, scratch, 0); - wrappedDataSink.write(scratch, 0, bytesToProcess); + castNonNull(cipher) + .update(data, offset + bytesProcessed, bytesToProcess, scratch, /* outOffset= */ 0); + wrappedDataSink.write(scratch, /* offset= */ 0, bytesToProcess); bytesProcessed += bytesToProcess; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java index 7a7af6b8a4..644338c8eb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream.crypto; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -34,7 +36,7 @@ public final class AesCipherDataSource implements DataSource { private final DataSource upstream; private final byte[] secretKey; - private @Nullable AesFlushingCipher cipher; + @Nullable private AesFlushingCipher cipher; public AesCipherDataSource(byte[] secretKey, DataSource upstream) { this.upstream = upstream; @@ -64,7 +66,7 @@ public final class AesCipherDataSource implements DataSource { if (read == C.RESULT_END_OF_INPUT) { return C.RESULT_END_OF_INPUT; } - cipher.updateInPlace(data, offset, read); + castNonNull(cipher).updateInPlace(data, offset, read); return read; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java index ff8841fa9c..3418f46ed0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream.crypto; +import androidx.annotation.Nullable; + /** * Utility functions for the crypto package. */ @@ -24,10 +26,10 @@ package com.google.android.exoplayer2.upstream.crypto; /** * Returns the hash value of the input as a long using the 64 bit FNV-1a hash function. The hash - * values produced by this function are less likely to collide than those produced by - * {@link #hashCode()}. + * values produced by this function are less likely to collide than those produced by {@link + * #hashCode()}. */ - public static long getFNV64Hash(String input) { + public static long getFNV64Hash(@Nullable String input) { if (input == null) { return 0; } From 10ee7d8e861132a0d937ebf585a437ff491cf1b4 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 May 2019 13:40:12 +0100 Subject: [PATCH 1302/1556] Remove more classes from nullness blacklist PiperOrigin-RevId: 249431027 --- .../exoplayer2/scheduler/Requirements.java | 3 +- .../scheduler/RequirementsWatcher.java | 10 ++++--- .../exoplayer2/text/CaptionStyleCompat.java | 30 ++++++++++++------- .../google/android/exoplayer2/text/Cue.java | 25 +++++++--------- .../exoplayer2/text/SubtitleOutputBuffer.java | 3 +- .../exoplayer2/text/webvtt/CssParser.java | 27 ++++++++++------- .../google/android/exoplayer2/util/Util.java | 15 ++++++++++ 7 files changed, 71 insertions(+), 42 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java index 30cf452572..882d9def3c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java @@ -27,6 +27,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.PowerManager; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -179,7 +180,7 @@ public final class Requirements implements Parcelable { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java index f0d0f37cdf..b9cbf681b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java @@ -27,7 +27,9 @@ import android.net.NetworkRequest; import android.os.Handler; import android.os.Looper; import android.os.PowerManager; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; /** @@ -57,10 +59,10 @@ public final class RequirementsWatcher { private final Requirements requirements; private final Handler handler; - private DeviceStatusChangeReceiver receiver; + @Nullable private DeviceStatusChangeReceiver receiver; @Requirements.RequirementFlags private int notMetRequirements; - private CapabilityValidatedCallback networkCallback; + @Nullable private CapabilityValidatedCallback networkCallback; /** * @param context Any context. @@ -111,7 +113,7 @@ public final class RequirementsWatcher { /** Stops watching for changes. */ public void stop() { - context.unregisterReceiver(receiver); + context.unregisterReceiver(Assertions.checkNotNull(receiver)); receiver = null; if (networkCallback != null) { unregisterNetworkCallback(); @@ -139,7 +141,7 @@ public final class RequirementsWatcher { if (Util.SDK_INT >= 21) { ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - connectivityManager.unregisterNetworkCallback(networkCallback); + connectivityManager.unregisterNetworkCallback(Assertions.checkNotNull(networkCallback)); networkCallback = null; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java b/library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java index b863d80c9a..a7ab93a6dd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java @@ -19,6 +19,7 @@ import android.annotation.TargetApi; import android.graphics.Color; import android.graphics.Typeface; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import android.view.accessibility.CaptioningManager; import android.view.accessibility.CaptioningManager.CaptionStyle; import com.google.android.exoplayer2.util.Util; @@ -72,11 +73,15 @@ public final class CaptionStyleCompat { */ public static final int USE_TRACK_COLOR_SETTINGS = 1; - /** - * Default caption style. - */ - public static final CaptionStyleCompat DEFAULT = new CaptionStyleCompat( - Color.WHITE, Color.BLACK, Color.TRANSPARENT, EDGE_TYPE_NONE, Color.WHITE, null); + /** Default caption style. */ + public static final CaptionStyleCompat DEFAULT = + new CaptionStyleCompat( + Color.WHITE, + Color.BLACK, + Color.TRANSPARENT, + EDGE_TYPE_NONE, + Color.WHITE, + /* typeface= */ null); /** * The preferred foreground color. @@ -110,10 +115,8 @@ public final class CaptionStyleCompat { */ public final int edgeColor; - /** - * The preferred typeface. - */ - public final Typeface typeface; + /** The preferred typeface, or {@code null} if unspecified. */ + @Nullable public final Typeface typeface; /** * Creates a {@link CaptionStyleCompat} equivalent to a provided {@link CaptionStyle}. @@ -141,8 +144,13 @@ public final class CaptionStyleCompat { * @param edgeColor See {@link #edgeColor}. * @param typeface See {@link #typeface}. */ - public CaptionStyleCompat(int foregroundColor, int backgroundColor, int windowColor, - @EdgeType int edgeType, int edgeColor, Typeface typeface) { + public CaptionStyleCompat( + int foregroundColor, + int backgroundColor, + int windowColor, + @EdgeType int edgeType, + int edgeColor, + @Nullable Typeface typeface) { this.foregroundColor = foregroundColor; this.backgroundColor = backgroundColor; this.windowColor = windowColor; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java index 4b54b3ea9a..29facdb210 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.text; import android.graphics.Bitmap; import android.graphics.Color; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import android.text.Layout.Alignment; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -111,17 +112,13 @@ public class Cue { * The cue text, or null if this is an image cue. Note the {@link CharSequence} may be decorated * with styling spans. */ - public final CharSequence text; + @Nullable public final CharSequence text; - /** - * The alignment of the cue text within the cue box, or null if the alignment is undefined. - */ - public final Alignment textAlignment; + /** The alignment of the cue text within the cue box, or null if the alignment is undefined. */ + @Nullable public final Alignment textAlignment; - /** - * The cue image, or null if this is a text cue. - */ - public final Bitmap bitmap; + /** The cue image, or null if this is a text cue. */ + @Nullable public final Bitmap bitmap; /** * The position of the {@link #lineAnchor} of the cue box within the viewport in the direction @@ -298,7 +295,7 @@ public class Cue { */ public Cue( CharSequence text, - Alignment textAlignment, + @Nullable Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, @@ -376,7 +373,7 @@ public class Cue { */ public Cue( CharSequence text, - Alignment textAlignment, + @Nullable Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, @@ -403,9 +400,9 @@ public class Cue { } private Cue( - CharSequence text, - Alignment textAlignment, - Bitmap bitmap, + @Nullable CharSequence text, + @Nullable Alignment textAlignment, + @Nullable Bitmap bitmap, float line, @LineType int lineType, @AnchorType int lineAnchor, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java index 75b7a01673..b34628b922 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.decoder.OutputBuffer; import java.util.List; @@ -24,7 +25,7 @@ import java.util.List; */ public abstract class SubtitleOutputBuffer extends OutputBuffer implements Subtitle { - private Subtitle subtitle; + @Nullable private Subtitle subtitle; private long subsampleOffsetUs; /** 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 81c362bda5..193b92678b 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 @@ -15,11 +15,11 @@ */ package com.google.android.exoplayer2.text.webvtt; +import androidx.annotation.Nullable; 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; @@ -52,13 +52,15 @@ import java.util.regex.Pattern; } /** - * Takes a CSS style block and consumes up to the first empty line found. Attempts to parse the - * contents of the style block and returns a {@link WebvttCssStyle} instance if successful, or - * {@code null} otherwise. + * Takes a CSS style block and consumes up to the first empty line. Attempts to parse the contents + * of the style block and returns a {@link WebvttCssStyle} instance if successful, or {@code null} + * otherwise. * * @param input The input from which the style block should be read. - * @return A {@link WebvttCssStyle} that represents the parsed block. + * @return A {@link WebvttCssStyle} that represents the parsed block, or {@code null} if parsing + * failed. */ + @Nullable public WebvttCssStyle parseBlock(ParsableByteArray input) { stringBuilder.setLength(0); int initialInputPosition = input.getPosition(); @@ -86,13 +88,14 @@ import java.util.regex.Pattern; } /** - * Returns a string containing the selector. The input is expected to have the form - * {@code ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional. + * Returns a string containing the selector. The input is expected to have the form {@code + * ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional. * * @param input From which the selector is obtained. - * @return A string containing the target, empty string if the selector is universal - * (targets all cues) or null if an error was encountered. + * @return A string containing the target, empty string if the selector is universal (targets all + * cues) or null if an error was encountered. */ + @Nullable private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) { skipWhitespaceAndComments(input); if (input.bytesLeft() < 5) { @@ -116,7 +119,7 @@ import java.util.regex.Pattern; target = readCueTarget(input); } token = parseNextToken(input, stringBuilder); - if (!")".equals(token) || token == null) { + if (!")".equals(token)) { return null; } return target; @@ -196,6 +199,7 @@ import java.util.regex.Pattern; } // Visible for testing. + @Nullable /* package */ static String parseNextToken(ParsableByteArray input, StringBuilder stringBuilder) { skipWhitespaceAndComments(input); if (input.bytesLeft() == 0) { @@ -237,6 +241,7 @@ import java.util.regex.Pattern; return (char) input.data[position]; } + @Nullable private static String parsePropertyValue(ParsableByteArray input, StringBuilder stringBuilder) { StringBuilder expressionBuilder = new StringBuilder(); String token; @@ -325,7 +330,7 @@ import java.util.regex.Pattern; style.setTargetTagName(tagAndIdDivision); } if (classDivision.length > 1) { - style.setTargetClasses(Arrays.copyOfRange(classDivision, 1, classDivision.length)); + style.setTargetClasses(Util.nullSafeArrayCopyOfRange(classDivision, 1, classDivision.length)); } } 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 97bcb68708..4dfb8b50d5 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 @@ -315,6 +315,21 @@ public final class Util { return Arrays.copyOf(input, length); } + /** + * Copies a subset of an array. + * + * @param input The input array. + * @param from The start the range to be copied, inclusive + * @param to The end of the range to be copied, exclusive. + * @return The copied array. + */ + @SuppressWarnings({"nullness:argument.type.incompatible", "nullness:return.type.incompatible"}) + public static T[] nullSafeArrayCopyOfRange(T[] input, int from, int to) { + Assertions.checkArgument(0 <= from); + Assertions.checkArgument(to <= input.length); + return Arrays.copyOfRange(input, from, to); + } + /** * Concatenates two non-null type arrays. * From f74d2294be0160fe1391b420a4e357c2dce5baf7 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 May 2019 13:47:59 +0100 Subject: [PATCH 1303/1556] Remove media-session extension nullness blacklist PiperOrigin-RevId: 249431620 --- extensions/mediasession/build.gradle | 1 + .../mediasession/MediaSessionConnector.java | 84 ++++++++++++------- .../RepeatModeActionProvider.java | 3 +- library/core/build.gradle | 1 - 4 files changed, 57 insertions(+), 32 deletions(-) diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle index 6c6ddf4ce4..7ee973723c 100644 --- a/extensions/mediasession/build.gradle +++ b/extensions/mediasession/build.gradle @@ -33,6 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') api 'androidx.media:media:1.0.1' + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion } ext { 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 d03e8fbdbf..9ec3886df5 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 @@ -52,6 +52,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; /** * Connects a {@link MediaSessionCompat} to a {@link Player}. @@ -359,7 +360,7 @@ public final class MediaSessionConnector { * @param extras Optional extras sent by a media controller. */ void onCustomAction( - Player player, ControlDispatcher controlDispatcher, String action, Bundle extras); + Player player, ControlDispatcher controlDispatcher, String action, @Nullable Bundle extras); /** * Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the media @@ -676,6 +677,7 @@ public final class MediaSessionConnector { */ public final void invalidateMediaSessionPlaybackState() { PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); + Player player = this.player; if (player == null) { builder.setActions(buildPrepareActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0); mediaSession.setPlaybackState(builder.build()); @@ -749,8 +751,8 @@ public final class MediaSessionConnector { * * @param commandReceiver The command receiver to register. */ - public void registerCustomCommandReceiver(CommandReceiver commandReceiver) { - if (!customCommandReceivers.contains(commandReceiver)) { + public void registerCustomCommandReceiver(@Nullable CommandReceiver commandReceiver) { + if (commandReceiver != null && !customCommandReceivers.contains(commandReceiver)) { customCommandReceivers.add(commandReceiver); } } @@ -760,18 +762,22 @@ public final class MediaSessionConnector { * * @param commandReceiver The command receiver to unregister. */ - public void unregisterCustomCommandReceiver(CommandReceiver commandReceiver) { - customCommandReceivers.remove(commandReceiver); + public void unregisterCustomCommandReceiver(@Nullable CommandReceiver commandReceiver) { + if (commandReceiver != null) { + customCommandReceivers.remove(commandReceiver); + } } - private void registerCommandReceiver(CommandReceiver commandReceiver) { - if (!commandReceivers.contains(commandReceiver)) { + private void registerCommandReceiver(@Nullable CommandReceiver commandReceiver) { + if (commandReceiver != null && !commandReceivers.contains(commandReceiver)) { commandReceivers.add(commandReceiver); } } - private void unregisterCommandReceiver(CommandReceiver commandReceiver) { - commandReceivers.remove(commandReceiver); + private void unregisterCommandReceiver(@Nullable CommandReceiver commandReceiver) { + if (commandReceiver != null) { + commandReceivers.remove(commandReceiver); + } } private long buildPrepareActions() { @@ -829,29 +835,43 @@ public final class MediaSessionConnector { } } + @EnsuresNonNullIf(result = true, expression = "player") private boolean canDispatchPlaybackAction(long action) { return player != null && (enabledPlaybackActions & action) != 0; } + @EnsuresNonNullIf(result = true, expression = "playbackPreparer") private boolean canDispatchToPlaybackPreparer(long action) { return playbackPreparer != null && (playbackPreparer.getSupportedPrepareActions() & action) != 0; } + @EnsuresNonNullIf( + result = true, + expression = {"player", "queueNavigator"}) private boolean canDispatchToQueueNavigator(long action) { return player != null && queueNavigator != null && (queueNavigator.getSupportedQueueNavigatorActions(player) & action) != 0; } + @EnsuresNonNullIf( + result = true, + expression = {"player", "ratingCallback"}) private boolean canDispatchSetRating() { return player != null && ratingCallback != null; } + @EnsuresNonNullIf( + result = true, + expression = {"player", "queueEditor"}) private boolean canDispatchQueueEdit() { return player != null && queueEditor != null; } + @EnsuresNonNullIf( + result = true, + expression = {"player", "mediaButtonEventHandler"}) private boolean canDispatchMediaButtonEvent() { return player != null && mediaButtonEventHandler != null; } @@ -941,38 +961,40 @@ public final class MediaSessionConnector { } } } - if (description.getTitle() != null) { - String title = String.valueOf(description.getTitle()); - builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title); - builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title); + CharSequence title = description.getTitle(); + if (title != null) { + String titleString = String.valueOf(title); + builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, titleString); + builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, titleString); } - if (description.getSubtitle() != null) { + CharSequence subtitle = description.getSubtitle(); + if (subtitle != null) { builder.putString( - MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, - String.valueOf(description.getSubtitle())); + MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, String.valueOf(subtitle)); } - if (description.getDescription() != null) { + CharSequence displayDescription = description.getDescription(); + if (displayDescription != null) { builder.putString( MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, - String.valueOf(description.getDescription())); + String.valueOf(displayDescription)); } - if (description.getIconBitmap() != null) { - builder.putBitmap( - MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, description.getIconBitmap()); + Bitmap iconBitmap = description.getIconBitmap(); + if (iconBitmap != null) { + builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, iconBitmap); } - if (description.getIconUri() != null) { + Uri iconUri = description.getIconUri(); + if (iconUri != null) { builder.putString( - MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, - String.valueOf(description.getIconUri())); + MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, String.valueOf(iconUri)); } - if (description.getMediaId() != null) { - builder.putString( - MediaMetadataCompat.METADATA_KEY_MEDIA_ID, description.getMediaId()); + String mediaId = description.getMediaId(); + if (mediaId != null) { + builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId); } - if (description.getMediaUri() != null) { + Uri mediaUri = description.getMediaUri(); + if (mediaUri != null) { builder.putString( - MediaMetadataCompat.METADATA_KEY_MEDIA_URI, - String.valueOf(description.getMediaUri())); + MediaMetadataCompat.METADATA_KEY_MEDIA_URI, String.valueOf(mediaUri)); } break; } @@ -993,6 +1015,7 @@ public final class MediaSessionConnector { @Override public void onTimelineChanged( Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { + Player player = Assertions.checkNotNull(MediaSessionConnector.this.player); int windowCount = player.getCurrentTimeline().getWindowCount(); int windowIndex = player.getCurrentWindowIndex(); if (queueNavigator != null) { @@ -1035,6 +1058,7 @@ public final class MediaSessionConnector { @Override public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + Player player = Assertions.checkNotNull(MediaSessionConnector.this.player); if (currentWindowIndex != player.getCurrentWindowIndex()) { if (queueNavigator != null) { queueNavigator.onCurrentWindowIndexChanged(player); diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java index 617b8781f4..5c969dd44d 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.mediasession; import android.content.Context; import android.os.Bundle; +import androidx.annotation.Nullable; import android.support.v4.media.session.PlaybackStateCompat; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.Player; @@ -65,7 +66,7 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus @Override public void onCustomAction( - Player player, ControlDispatcher controlDispatcher, String action, Bundle extras) { + Player player, ControlDispatcher controlDispatcher, String action, @Nullable Bundle extras) { int mode = player.getRepeatMode(); int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes); if (mode != proposedMode) { diff --git a/library/core/build.gradle b/library/core/build.gradle index f532ae0e6a..5b285411d0 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -60,7 +60,6 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.0.2' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion From 118218cc73efecb62d1ae3be39a490eaca5edd5c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 May 2019 13:55:46 +0100 Subject: [PATCH 1304/1556] Remove cronet extension nullness blacklist PiperOrigin-RevId: 249432337 --- extensions/cronet/build.gradle | 1 + .../ext/cronet/CronetDataSource.java | 148 ++++++++++-------- library/core/build.gradle | 1 + 3 files changed, 87 insertions(+), 63 deletions(-) diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 76972a3530..0808ad6c44 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -34,6 +34,7 @@ dependencies { api 'org.chromium.net:cronet-embedded:73.3683.76' implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.0.2' + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index ca196b1d2f..0ef20e79bd 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.ext.cronet; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.net.Uri; import androidx.annotation.Nullable; import android.text.TextUtils; @@ -41,6 +43,7 @@ import java.util.Map.Entry; import java.util.concurrent.Executor; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.chromium.net.CronetEngine; import org.chromium.net.CronetException; import org.chromium.net.NetworkException; @@ -118,7 +121,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { private final int readTimeoutMs; private final boolean resetTimeoutOnRedirects; private final boolean handleSetCookieRequests; - private final RequestProperties defaultRequestProperties; + @Nullable private final RequestProperties defaultRequestProperties; private final RequestProperties requestProperties; private final ConditionVariable operation; private final Clock clock; @@ -130,18 +133,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { // Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible // to reads made by the Cronet thread. - private UrlRequest currentUrlRequest; - private DataSpec currentDataSpec; + @Nullable private UrlRequest currentUrlRequest; + @Nullable private DataSpec currentDataSpec; // Reference written and read by calling thread only. Passed to Cronet thread as a local variable. // operation.open() calls ensure writes into the buffer are visible to reads made by the calling // thread. - private ByteBuffer readBuffer; + @Nullable private ByteBuffer readBuffer; // Written from the Cronet thread only. operation.open() calls ensure writes are visible to reads // made by the calling thread. - private UrlResponseInfo responseInfo; - private IOException exception; + @Nullable private UrlResponseInfo responseInfo; + @Nullable private IOException exception; private boolean finished; private volatile long currentConnectTimeoutMs; @@ -197,7 +200,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * @param connectTimeoutMs The connection timeout, in milliseconds. * @param readTimeoutMs The read timeout, in milliseconds. * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. - * @param defaultRequestProperties The default request properties to be used. + * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to + * the server as HTTP headers on every request. */ public CronetDataSource( CronetEngine cronetEngine, @@ -206,7 +210,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, - RequestProperties defaultRequestProperties) { + @Nullable RequestProperties defaultRequestProperties) { this( cronetEngine, executor, @@ -232,7 +236,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * @param connectTimeoutMs The connection timeout, in milliseconds. * @param readTimeoutMs The read timeout, in milliseconds. * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. - * @param defaultRequestProperties The default request properties to be used. + * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to + * the server as HTTP headers on every request. * @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to * the redirect url in the "Cookie" header. */ @@ -243,7 +248,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, - RequestProperties defaultRequestProperties, + @Nullable RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) { this( cronetEngine, @@ -265,7 +270,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock, - RequestProperties defaultRequestProperties, + @Nullable RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) { super(/* isNetwork= */ true); this.urlRequestCallback = new UrlRequestCallback(); @@ -305,6 +310,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } @Override + @Nullable public Uri getUri() { return responseInfo == null ? null : Uri.parse(responseInfo.getUrl()); } @@ -317,22 +323,23 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { operation.close(); resetConnectTimeout(); currentDataSpec = dataSpec; + UrlRequest urlRequest; try { - currentUrlRequest = buildRequestBuilder(dataSpec).build(); + urlRequest = buildRequestBuilder(dataSpec).build(); + currentUrlRequest = urlRequest; } catch (IOException e) { - throw new OpenException(e, currentDataSpec, Status.IDLE); + throw new OpenException(e, dataSpec, Status.IDLE); } - currentUrlRequest.start(); + urlRequest.start(); transferInitializing(dataSpec); try { boolean connectionOpened = blockUntilConnectTimeout(); if (exception != null) { - throw new OpenException(exception, currentDataSpec, getStatus(currentUrlRequest)); + throw new OpenException(exception, dataSpec, getStatus(urlRequest)); } else if (!connectionOpened) { // The timeout was reached before the connection was opened. - throw new OpenException( - new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest)); + throw new OpenException(new SocketTimeoutException(), dataSpec, getStatus(urlRequest)); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -340,6 +347,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } // Check for a valid response code. + UrlResponseInfo responseInfo = Assertions.checkNotNull(this.responseInfo); int responseCode = responseInfo.getHttpStatusCode(); if (responseCode < 200 || responseCode > 299) { InvalidResponseCodeException exception = @@ -347,7 +355,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { responseCode, responseInfo.getHttpStatusText(), responseInfo.getAllHeaders(), - currentDataSpec); + dataSpec); if (responseCode == 416) { exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE)); } @@ -358,8 +366,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { if (contentTypePredicate != null) { List contentTypeHeaders = responseInfo.getAllHeaders().get(CONTENT_TYPE); String contentType = isEmpty(contentTypeHeaders) ? null : contentTypeHeaders.get(0); - if (!contentTypePredicate.evaluate(contentType)) { - throw new InvalidContentTypeException(contentType, currentDataSpec); + if (contentType != null && !contentTypePredicate.evaluate(contentType)) { + throw new InvalidContentTypeException(contentType, dataSpec); } } @@ -378,7 +386,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } else { // If the response is compressed then the content length will be that of the compressed data // which isn't what we want. Always use the dataSpec length in this case. - bytesRemaining = currentDataSpec.length; + bytesRemaining = dataSpec.length; } opened = true; @@ -397,15 +405,17 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { return C.RESULT_END_OF_INPUT; } + ByteBuffer readBuffer = this.readBuffer; if (readBuffer == null) { readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES); readBuffer.limit(0); + this.readBuffer = readBuffer; } while (!readBuffer.hasRemaining()) { // Fill readBuffer with more data from Cronet. operation.close(); readBuffer.clear(); - currentUrlRequest.read(readBuffer); + castNonNull(currentUrlRequest).read(readBuffer); try { if (!operation.block(readTimeoutMs)) { throw new SocketTimeoutException(); @@ -413,20 +423,23 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } catch (InterruptedException e) { // The operation is ongoing so replace readBuffer to avoid it being written to by this // operation during a subsequent request. - readBuffer = null; + this.readBuffer = null; Thread.currentThread().interrupt(); throw new HttpDataSourceException( - new InterruptedIOException(e), currentDataSpec, HttpDataSourceException.TYPE_READ); + new InterruptedIOException(e), + castNonNull(currentDataSpec), + HttpDataSourceException.TYPE_READ); } catch (SocketTimeoutException e) { // The operation is ongoing so replace readBuffer to avoid it being written to by this // operation during a subsequent request. - readBuffer = null; - throw new HttpDataSourceException(e, currentDataSpec, HttpDataSourceException.TYPE_READ); + this.readBuffer = null; + throw new HttpDataSourceException( + e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ); } if (exception != null) { - throw new HttpDataSourceException(exception, currentDataSpec, - HttpDataSourceException.TYPE_READ); + throw new HttpDataSourceException( + exception, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ); } else if (finished) { bytesRemaining = 0; return C.RESULT_END_OF_INPUT; @@ -631,7 +644,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { return statusHolder[0]; } - private static boolean isEmpty(List list) { + @EnsuresNonNullIf(result = false, expression = "#1") + private static boolean isEmpty(@Nullable List list) { return list == null || list.isEmpty(); } @@ -643,13 +657,15 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { if (request != currentUrlRequest) { return; } - if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) { + UrlRequest urlRequest = Assertions.checkNotNull(currentUrlRequest); + DataSpec dataSpec = Assertions.checkNotNull(currentDataSpec); + if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) { int responseCode = info.getHttpStatusCode(); // The industry standard is to disregard POST redirects when the status code is 307 or 308. if (responseCode == 307 || responseCode == 308) { exception = new InvalidResponseCodeException( - responseCode, info.getHttpStatusText(), info.getAllHeaders(), currentDataSpec); + responseCode, info.getHttpStatusText(), info.getAllHeaders(), dataSpec); operation.open(); return; } @@ -658,40 +674,46 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { resetConnectTimeout(); } - Map> headers = info.getAllHeaders(); - if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) { + if (!handleSetCookieRequests) { request.followRedirect(); - } else { - currentUrlRequest.cancel(); - DataSpec redirectUrlDataSpec; - if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) { - // For POST redirects that aren't 307 or 308, the redirect is followed but request is - // transformed into a GET. - redirectUrlDataSpec = - new DataSpec( - Uri.parse(newLocationUrl), - DataSpec.HTTP_METHOD_GET, - /* httpBody= */ null, - currentDataSpec.absoluteStreamPosition, - currentDataSpec.position, - currentDataSpec.length, - currentDataSpec.key, - currentDataSpec.flags); - } else { - redirectUrlDataSpec = currentDataSpec.withUri(Uri.parse(newLocationUrl)); - } - UrlRequest.Builder requestBuilder; - try { - requestBuilder = buildRequestBuilder(redirectUrlDataSpec); - } catch (IOException e) { - exception = e; - return; - } - String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE)); - attachCookies(requestBuilder, cookieHeadersValue); - currentUrlRequest = requestBuilder.build(); - currentUrlRequest.start(); + return; } + + List setCookieHeaders = info.getAllHeaders().get(SET_COOKIE); + if (isEmpty(setCookieHeaders)) { + request.followRedirect(); + return; + } + + urlRequest.cancel(); + DataSpec redirectUrlDataSpec; + if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) { + // For POST redirects that aren't 307 or 308, the redirect is followed but request is + // transformed into a GET. + redirectUrlDataSpec = + new DataSpec( + Uri.parse(newLocationUrl), + DataSpec.HTTP_METHOD_GET, + /* httpBody= */ null, + dataSpec.absoluteStreamPosition, + dataSpec.position, + dataSpec.length, + dataSpec.key, + dataSpec.flags); + } else { + redirectUrlDataSpec = dataSpec.withUri(Uri.parse(newLocationUrl)); + } + UrlRequest.Builder requestBuilder; + try { + requestBuilder = buildRequestBuilder(redirectUrlDataSpec); + } catch (IOException e) { + exception = e; + return; + } + String cookieHeadersValue = parseCookies(setCookieHeaders); + attachCookies(requestBuilder, cookieHeadersValue); + currentUrlRequest = requestBuilder.build(); + currentUrlRequest.start(); } @Override diff --git a/library/core/build.gradle b/library/core/build.gradle index 5b285411d0..f532ae0e6a 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -60,6 +60,7 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.0.2' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion From cfefdbc134101e6efca35bd5b781f7eaa8020c7d Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 22 May 2019 14:54:41 +0100 Subject: [PATCH 1305/1556] Release DownloadHelper automatically if preparation failed. This prevents further unexpected updates if the MediaSource happens to finish its preparation at a later point. Issue:#5915 PiperOrigin-RevId: 249439246 --- RELEASENOTES.md | 3 +++ .../com/google/android/exoplayer2/offline/DownloadHelper.java | 1 + 2 files changed, 4 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ed9635a340..06c1ed7f80 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,9 @@ ([#5891](https://github.com/google/ExoPlayer/issues/5891)). * Add ProgressUpdateListener to PlayerControlView ([#5834](https://github.com/google/ExoPlayer/issues/5834)). +* Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the + preparation of the `DownloadHelper` failed + ([#5915](https://github.com/google/ExoPlayer/issues/5915)). ### 2.10.1 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index d2b7bd84d2..e7cf87ed6e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -943,6 +943,7 @@ public final class DownloadHelper { downloadHelper.onMediaPrepared(); return true; case DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED: + release(); downloadHelper.onMediaPreparationFailed((IOException) Util.castNonNull(msg.obj)); return true; default: From 073256ead06d96b60ad878a07f93af930e5a259f Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 22 May 2019 19:44:46 +0100 Subject: [PATCH 1306/1556] improve issue templates PiperOrigin-RevId: 249489446 --- .github/ISSUE_TEMPLATE/bug.md | 9 ++++++--- .github/ISSUE_TEMPLATE/content_not_playing.md | 5 ++++- .github/ISSUE_TEMPLATE/feature_request.md | 5 +++-- .github/ISSUE_TEMPLATE/question.md | 8 ++++++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 690069ffa8..a4996278bd 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -8,9 +8,12 @@ assignees: '' Before filing a bug: ----------------------- -- Search existing issues, including issues that are closed. -- Consult our FAQs, supported devices and supported formats pages. These can be - found at https://exoplayer.dev/. +- Search existing issues, including issues that are closed: + https://github.com/google/ExoPlayer/issues?q=is%3Aissue +- Consult our developer website, which can be found at https://exoplayer.dev/. + It provides detailed information about supported formats and devices. +- Learn how to create useful log output by using the EventLogger: + https://exoplayer.dev/listening-to-player-events.html#using-eventlogger - Rule out issues in your own code. A good way to do this is to try and reproduce the issue in the ExoPlayer demo app. Information about the ExoPlayer demo app can be found here: diff --git a/.github/ISSUE_TEMPLATE/content_not_playing.md b/.github/ISSUE_TEMPLATE/content_not_playing.md index f326e7cd46..ff29f3a7d1 100644 --- a/.github/ISSUE_TEMPLATE/content_not_playing.md +++ b/.github/ISSUE_TEMPLATE/content_not_playing.md @@ -8,9 +8,12 @@ assignees: '' Before filing a content issue: ------------------------------ -- Search existing issues, including issues that are closed. +- Search existing issues, including issues that are closed: + https://github.com/google/ExoPlayer/issues?q=is%3Aissue - Consult our supported formats page, which can be found at https://exoplayer.dev/supported-formats.html. +- Learn how to create useful log output by using the EventLogger: + https://exoplayer.dev/listening-to-player-events.html#using-eventlogger - Try playing your content in the ExoPlayer demo app. Information about the ExoPlayer demo app can be found here: http://exoplayer.dev/demo-application.html. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 089de35910..d481de33ce 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -8,8 +8,9 @@ assignees: '' Before filing a feature request: ----------------------- -- Search existing open issues, specifically with the label ‘enhancement’. -- Search existing pull requests. +- Search existing open issues, specifically with the label ‘enhancement’: + https://github.com/google/ExoPlayer/labels/enhancement +- Search existing pull requests: https://github.com/google/ExoPlayer/pulls When filing a feature request: ----------------------- diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 3ed569862f..a68e4e70e1 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -12,8 +12,12 @@ Before filing a question: a general Android development question, please do so on Stack Overflow. - Search existing issues, including issues that are closed. It’s often the quickest way to get an answer! -- Consult our FAQs, developer guide and the class reference of ExoPlayer. These - can be found at https://exoplayer.dev/. + https://github.com/google/ExoPlayer/issues?q=is%3Aissue +- Consult our developer website, which can be found at https://exoplayer.dev/. + It provides detailed information about supported formats, devices as well as + information about how to use the ExoPlayer library. +- The ExoPlayer library Javadoc can be found at + https://exoplayer.dev/doc/reference/ When filing a question: ----------------------- From 2f12374f1a4128a7844c3c6d804e08f0ecb7b53f Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 May 2019 10:56:58 +0100 Subject: [PATCH 1307/1556] Fix IndexOutOfBounds when there are no available codecs PiperOrigin-RevId: 249610014 --- .../exoplayer2/mediacodec/MediaCodecRenderer.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 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 06b76781b4..730868987a 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 @@ -53,7 +53,6 @@ import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -742,11 +741,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { try { List allAvailableCodecInfos = getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder); + availableCodecInfos = new ArrayDeque<>(); if (enableDecoderFallback) { - availableCodecInfos = new ArrayDeque<>(allAvailableCodecInfos); - } else { - availableCodecInfos = - new ArrayDeque<>(Collections.singletonList(allAvailableCodecInfos.get(0))); + availableCodecInfos.addAll(allAvailableCodecInfos); + } else if (!allAvailableCodecInfos.isEmpty()) { + availableCodecInfos.add(allAvailableCodecInfos.get(0)); } preferredDecoderInitializationException = null; } catch (DecoderQueryException e) { From 8d329fb41f19cb9303f90158c6b84ced1af955d3 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 23 May 2019 13:24:01 +0100 Subject: [PATCH 1308/1556] Move DefaultDrmSession resource acquisition to acquire PiperOrigin-RevId: 249624318 --- .../exoplayer2/drm/DefaultDrmSession.java | 39 ++++++++++--------- .../drm/OfflineLicenseHelperTest.java | 7 ++++ 2 files changed, 27 insertions(+), 19 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 215a48fc50..94f5affb39 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 @@ -74,7 +74,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; public interface ReleaseCallback { /** - * Called when the session is released. + * Called immediately after releasing session resources. * * @param session The session. */ @@ -85,7 +85,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private static final int MSG_PROVISION = 0; private static final int MSG_KEYS = 1; - private static final int MAX_LICENSE_DURATION_TO_RENEW = 60; + private static final int MAX_LICENSE_DURATION_TO_RENEW_SECONDS = 60; /** The DRM scheme datas, or null if this session uses offline keys. */ public final @Nullable List schemeDatas; @@ -104,10 +104,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private @DrmSession.State int state; private int openCount; - private HandlerThread requestHandlerThread; - private PostRequestHandler postRequestHandler; - private @Nullable T mediaCrypto; - private @Nullable DrmSessionException lastException; + @Nullable private HandlerThread requestHandlerThread; + @Nullable private PostRequestHandler postRequestHandler; + @Nullable private T mediaCrypto; + @Nullable private DrmSessionException lastException; private byte @MonotonicNonNull [] sessionId; private byte @MonotonicNonNull [] offlineLicenseKeySetId; @@ -166,35 +166,31 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; this.initialDrmRequestRetryCount = initialDrmRequestRetryCount; this.eventDispatcher = eventDispatcher; state = STATE_OPENING; - postResponseHandler = new PostResponseHandler(playbackLooper); - requestHandlerThread = new HandlerThread("DrmRequestHandler"); - requestHandlerThread.start(); - postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); } // Life cycle. public void acquire() { if (++openCount == 1) { - if (state == STATE_ERROR) { - return; - } + requestHandlerThread = new HandlerThread("DrmRequestHandler"); + requestHandlerThread.start(); + postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); if (openInternal(true)) { doLicense(true); } } } - // Assigning null to various non-null variables for clean-up. Class won't be used after release. @SuppressWarnings("assignment.type.incompatible") public void release() { if (--openCount == 0) { + // Assigning null to various non-null variables for clean-up. state = STATE_RELEASED; postResponseHandler.removeCallbacksAndMessages(null); - postRequestHandler.removeCallbacksAndMessages(null); + Util.castNonNull(postRequestHandler).removeCallbacksAndMessages(null); postRequestHandler = null; - requestHandlerThread.quit(); + Util.castNonNull(requestHandlerThread).quit(); requestHandlerThread = null; mediaCrypto = null; lastException = null; @@ -227,7 +223,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; public void provision() { currentProvisionRequest = mediaDrm.getProvisionRequest(); - postRequestHandler.post(MSG_PROVISION, currentProvisionRequest, /* allowRetry= */ true); + Util.castNonNull(postRequestHandler) + .post( + MSG_PROVISION, + Assertions.checkNotNull(currentProvisionRequest), + /* allowRetry= */ true); } public void onProvisionCompleted() { @@ -335,7 +335,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } else if (state == STATE_OPENED_WITH_KEYS || restoreKeys()) { long licenseDurationRemainingSec = getLicenseDurationRemainingSec(); if (mode == DefaultDrmSessionManager.MODE_PLAYBACK - && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) { + && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW_SECONDS) { Log.d( TAG, "Offline license has expired or will expire soon. " @@ -398,7 +398,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; try { currentKeyRequest = mediaDrm.getKeyRequest(scope, schemeDatas, type, optionalKeyRequestParameters); - postRequestHandler.post(MSG_KEYS, currentKeyRequest, allowRetry); + Util.castNonNull(postRequestHandler) + .post(MSG_KEYS, Assertions.checkNotNull(currentKeyRequest), allowRetry); } catch (Exception e) { onKeysError(e); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 83ca752114..d6b0b5ba15 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer2.drm; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; @@ -25,6 +27,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import java.util.HashMap; +import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -46,6 +49,10 @@ public class OfflineLicenseHelperTest { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3}); + when(mediaDrm.getKeyRequest( + nullable(byte[].class), nullable(List.class), anyInt(), nullable(HashMap.class))) + .thenReturn( + new ExoMediaDrm.KeyRequest(/* data= */ new byte[0], /* licenseServerUrl= */ "")); offlineLicenseHelper = new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback, null); } From 3314391932394feef441d6f619651c6ea5f3d5f7 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 23 May 2019 13:29:56 +0100 Subject: [PATCH 1309/1556] Add basic DRM support to CastPlayer's demo app PiperOrigin-RevId: 249624829 --- RELEASENOTES.md | 1 + .../DefaultReceiverPlayerManager.java | 46 +++++++++++++-- .../android/exoplayer2/castdemo/DemoUtil.java | 56 ++++++++++--------- .../exoplayer2/castdemo/MainActivity.java | 13 ++++- .../ext/cast/DefaultCastOptionsProvider.java | 26 ++++++++- 5 files changed, 108 insertions(+), 34 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 06c1ed7f80..deda085f40 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,7 @@ ### dev-v2 (not yet released) ### +* Add basic DRM support to the Cast demo app. * Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s ([#5779](https://github.com/google/ExoPlayer/issues/5779)). * Assume that encrypted content requires secure decoders in renderer support diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java index fcee88ec49..a837bd77e5 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java @@ -44,11 +44,14 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; import java.util.ArrayList; +import org.json.JSONException; +import org.json.JSONObject; /** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */ /* package */ class DefaultReceiverPlayerManager @@ -394,12 +397,47 @@ import java.util.ArrayList; private static MediaQueueItem buildMediaQueueItem(MediaItem item) { MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title); - MediaInfo mediaInfo = + MediaInfo.Builder mediaInfoBuilder = new MediaInfo.Builder(item.media.uri.toString()) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setContentType(item.mimeType) - .setMetadata(movieMetadata) - .build(); - return new MediaQueueItem.Builder(mediaInfo).build(); + .setMetadata(movieMetadata); + if (!item.drmSchemes.isEmpty()) { + MediaItem.DrmScheme scheme = item.drmSchemes.get(0); + try { + // This configuration is only intended for testing and should *not* be used in production + // environments. See comment in the Cast Demo app's options provider. + JSONObject drmConfiguration = getDrmConfigurationJson(scheme); + if (drmConfiguration != null) { + mediaInfoBuilder.setCustomData(drmConfiguration); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + return new MediaQueueItem.Builder(mediaInfoBuilder.build()).build(); + } + + @Nullable + private static JSONObject getDrmConfigurationJson(MediaItem.DrmScheme scheme) + throws JSONException { + String drmScheme; + if (C.WIDEVINE_UUID.equals(scheme.uuid)) { + drmScheme = "widevine"; + } else if (C.PLAYREADY_UUID.equals(scheme.uuid)) { + drmScheme = "playready"; + } else { + return null; + } + MediaItem.UriBundle licenseServer = Assertions.checkNotNull(scheme.licenseServer); + JSONObject exoplayerConfig = + new JSONObject().put("withCredentials", false).put("protectionSystem", drmScheme); + if (!licenseServer.uri.equals(Uri.EMPTY)) { + exoplayerConfig.put("licenseUrl", licenseServer.uri.toString()); + } + if (!licenseServer.requestHeaders.isEmpty()) { + exoplayerConfig.put("headers", new JSONObject(licenseServer.requestHeaders)); + } + return new JSONObject().put("exoPlayerConfig", exoplayerConfig); } } diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index 9625304252..9599da15cb 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -15,13 +15,13 @@ */ package com.google.android.exoplayer2.castdemo; -import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.UUID; /** Utility methods and constants for the Cast demo application. */ @@ -30,44 +30,25 @@ import java.util.UUID; /** Represents a media sample. */ public static final class Sample { - /** The uri of the media content. */ + /** The URI of the media content. */ public final String uri; /** The name of the sample. */ public final String name; /** The mime type of the sample media content. */ public final String mimeType; - /** - * The {@link UUID} of the DRM scheme that protects the content, or null if the content is not - * DRM-protected. - */ - @Nullable public final UUID drmSchemeUuid; - /** - * The url from which players should obtain DRM licenses, or null if the content is not - * DRM-protected. - */ - @Nullable public final Uri licenseServerUri; + /** Data to configure DRM license acquisition. May be null if content is not DRM-protected. */ + @Nullable public final DrmConfiguration drmConfiguration; - /** - * @param uri See {@link #uri}. - * @param name See {@link #name}. - * @param mimeType See {@link #mimeType}. - */ public Sample(String uri, String name, String mimeType) { - this(uri, name, mimeType, /* drmSchemeUuid= */ null, /* licenseServerUriString= */ null); + this(uri, name, mimeType, /* drmConfiguration= */ null); } public Sample( - String uri, - String name, - String mimeType, - @Nullable UUID drmSchemeUuid, - @Nullable String licenseServerUriString) { + String uri, String name, String mimeType, @Nullable DrmConfiguration drmConfiguration) { this.uri = uri; this.name = name; this.mimeType = mimeType; - this.drmSchemeUuid = drmSchemeUuid; - this.licenseServerUri = - licenseServerUriString != null ? Uri.parse(licenseServerUriString) : null; + this.drmConfiguration = drmConfiguration; } @Override @@ -76,6 +57,29 @@ import java.util.UUID; } } + /** Holds information required to play DRM-protected content. */ + public static final class DrmConfiguration { + + /** The {@link UUID} of the DRM scheme that protects the content. */ + public final UUID drmSchemeUuid; + /** + * The URI from which players should obtain DRM licenses. May be null if the license server URI + * is provided as part of the media. + */ + @Nullable public final String licenseServerUri; + /** HTTP request headers to include the in DRM license requests. */ + public final Map httpRequestHeaders; + + public DrmConfiguration( + UUID drmSchemeUuid, + @Nullable String licenseServerUri, + Map httpRequestHeaders) { + this.drmSchemeUuid = drmSchemeUuid; + this.licenseServerUri = licenseServerUri; + this.httpRequestHeaders = httpRequestHeaders; + } + } + public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD; public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8; public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS; diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 17eeed2da7..5ed434eed6 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.castdemo; import android.content.Context; +import android.net.Uri; import android.os.Bundle; import androidx.core.graphics.ColorUtils; import androidx.appcompat.app.AlertDialog; @@ -36,6 +37,7 @@ import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider; import com.google.android.exoplayer2.ext.cast.MediaItem; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; @@ -121,6 +123,7 @@ public class MainActivity extends AppCompatActivity String applicationId = castContext.getCastOptions().getReceiverApplicationId(); switch (applicationId) { case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID: + case DefaultCastOptionsProvider.APP_ID_DEFAULT_RECEIVER_WITH_DRM: playerManager = new DefaultReceiverPlayerManager( /* listener= */ this, @@ -202,11 +205,17 @@ public class MainActivity extends AppCompatActivity .setMedia(sample.uri) .setTitle(sample.name) .setMimeType(sample.mimeType); - if (sample.drmSchemeUuid != null) { + DemoUtil.DrmConfiguration drmConfiguration = sample.drmConfiguration; + if (drmConfiguration != null) { mediaItemBuilder.setDrmSchemes( Collections.singletonList( new MediaItem.DrmScheme( - sample.drmSchemeUuid, new MediaItem.UriBundle(sample.licenseServerUri)))); + drmConfiguration.drmSchemeUuid, + new MediaItem.UriBundle( + drmConfiguration.licenseServerUri != null + ? Uri.parse(drmConfiguration.licenseServerUri) + : Uri.EMPTY, + drmConfiguration.httpRequestHeaders)))); } playerManager.addItem(mediaItemBuilder.build()); mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java index 06f0bec971..5aed1373e5 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java @@ -27,11 +27,33 @@ import java.util.List; */ public final class DefaultCastOptionsProvider implements OptionsProvider { + /** + * App id of the Default Media Receiver app. Apps that do not require DRM support may use this + * receiver receiver app ID. + * + *

    See https://developers.google.com/cast/docs/caf_receiver/#default_media_receiver. + */ + public static final String APP_ID_DEFAULT_RECEIVER = + CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID; + + /** + * App id for receiver app with rudimentary support for DRM. + * + *

    This app id is only suitable for ExoPlayer's Cast Demo app, and it is not intended for + * production use. In order to use DRM, custom receiver apps should be used. For environments that + * do not require DRM, the default receiver app should be used (see {@link + * #APP_ID_DEFAULT_RECEIVER}). + */ + // TODO: Add a documentation resource link for DRM support in the receiver app [Internal ref: + // b/128603245]. + public static final String APP_ID_DEFAULT_RECEIVER_WITH_DRM = "A12D4273"; + @Override public CastOptions getCastOptions(Context context) { return new CastOptions.Builder() - .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID) - .setStopReceiverApplicationWhenEndingSession(true).build(); + .setReceiverApplicationId(APP_ID_DEFAULT_RECEIVER_WITH_DRM) + .setStopReceiverApplicationWhenEndingSession(true) + .build(); } @Override From 14c46bc4062ebc2cf45f96138dc8a5e36bf41da5 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 May 2019 14:59:17 +0100 Subject: [PATCH 1310/1556] Remove contentTypePredicate from DataSource constructors The only known use case for contentTypePredicate is to catch the case when a paywall web page is returned via a DataSource, rather than the data that was being requested. These days streaming providers should be using HTTPS, where this problem does not exist. Devices have also gotten a lot better at showing their own notifications when paywalls are detected, which largely mitigates the need for the app to show a more optimal error message or redirect the user to a browser. It therefore makes sense to deprioritize this feature. In particular by removing the arg from constructors, where nearly all applications are probably passing null. PiperOrigin-RevId: 249634594 --- .../ext/cronet/CronetDataSource.java | 117 +++++++++++++++--- .../ext/cronet/CronetDataSourceFactory.java | 83 +++++-------- .../ext/cronet/CronetDataSourceTest.java | 38 +++--- .../ext/okhttp/OkHttpDataSource.java | 58 +++++++-- .../ext/okhttp/OkHttpDataSourceFactory.java | 1 - .../upstream/DefaultHttpDataSource.java | 75 ++++++++++- .../DefaultHttpDataSourceFactory.java | 1 - 7 files changed, 270 insertions(+), 103 deletions(-) diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index 0ef20e79bd..dd10e5bb66 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -116,7 +116,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { private final CronetEngine cronetEngine; private final Executor executor; - @Nullable private final Predicate contentTypePredicate; private final int connectTimeoutMs; private final int readTimeoutMs; private final boolean resetTimeoutOnRedirects; @@ -126,6 +125,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { private final ConditionVariable operation; private final Clock clock; + @Nullable private Predicate contentTypePredicate; + // Accessed by the calling thread only. private boolean opened; private long bytesToSkip; @@ -158,7 +159,78 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * handling is a fast operation when using a direct executor. */ public CronetDataSource(CronetEngine cronetEngine, Executor executor) { - this(cronetEngine, executor, /* contentTypePredicate= */ null); + this( + cronetEngine, + executor, + DEFAULT_CONNECT_TIMEOUT_MILLIS, + DEFAULT_READ_TIMEOUT_MILLIS, + /* resetTimeoutOnRedirects= */ false, + /* defaultRequestProperties= */ null); + } + + /** + * @param cronetEngine A CronetEngine. + * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may + * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread + * hop from Cronet's internal network thread to the response handling thread. However, to + * avoid slowing down overall network performance, care must be taken to make sure response + * handling is a fast operation when using a direct executor. + * @param connectTimeoutMs The connection timeout, in milliseconds. + * @param readTimeoutMs The read timeout, in milliseconds. + * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. + * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the + * server as HTTP headers on every request. + */ + public CronetDataSource( + CronetEngine cronetEngine, + Executor executor, + int connectTimeoutMs, + int readTimeoutMs, + boolean resetTimeoutOnRedirects, + @Nullable RequestProperties defaultRequestProperties) { + this( + cronetEngine, + executor, + connectTimeoutMs, + readTimeoutMs, + resetTimeoutOnRedirects, + Clock.DEFAULT, + defaultRequestProperties, + /* handleSetCookieRequests= */ false); + } + + /** + * @param cronetEngine A CronetEngine. + * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may + * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread + * hop from Cronet's internal network thread to the response handling thread. However, to + * avoid slowing down overall network performance, care must be taken to make sure response + * handling is a fast operation when using a direct executor. + * @param connectTimeoutMs The connection timeout, in milliseconds. + * @param readTimeoutMs The read timeout, in milliseconds. + * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. + * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the + * server as HTTP headers on every request. + * @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to + * the redirect url in the "Cookie" header. + */ + public CronetDataSource( + CronetEngine cronetEngine, + Executor executor, + int connectTimeoutMs, + int readTimeoutMs, + boolean resetTimeoutOnRedirects, + @Nullable RequestProperties defaultRequestProperties, + boolean handleSetCookieRequests) { + this( + cronetEngine, + executor, + connectTimeoutMs, + readTimeoutMs, + resetTimeoutOnRedirects, + Clock.DEFAULT, + defaultRequestProperties, + handleSetCookieRequests); } /** @@ -171,7 +243,10 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then an {@link InvalidContentTypeException} is thrown from {@link * #open(DataSpec)}. + * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor)} and {@link + * #setContentTypePredicate(Predicate)}. */ + @Deprecated public CronetDataSource( CronetEngine cronetEngine, Executor executor, @@ -182,9 +257,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { contentTypePredicate, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, - false, - null, - false); + /* resetTimeoutOnRedirects= */ false, + /* defaultRequestProperties= */ null); } /** @@ -200,9 +274,12 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * @param connectTimeoutMs The connection timeout, in milliseconds. * @param readTimeoutMs The read timeout, in milliseconds. * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. - * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to - * the server as HTTP headers on every request. + * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the + * server as HTTP headers on every request. + * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean, + * RequestProperties)} and {@link #setContentTypePredicate(Predicate)}. */ + @Deprecated public CronetDataSource( CronetEngine cronetEngine, Executor executor, @@ -218,9 +295,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, - Clock.DEFAULT, defaultRequestProperties, - false); + /* handleSetCookieRequests= */ false); } /** @@ -236,11 +312,14 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * @param connectTimeoutMs The connection timeout, in milliseconds. * @param readTimeoutMs The read timeout, in milliseconds. * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. - * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to - * the server as HTTP headers on every request. + * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the + * server as HTTP headers on every request. * @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to * the redirect url in the "Cookie" header. + * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean, + * RequestProperties, boolean)} and {@link #setContentTypePredicate(Predicate)}. */ + @Deprecated public CronetDataSource( CronetEngine cronetEngine, Executor executor, @@ -253,19 +332,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { this( cronetEngine, executor, - contentTypePredicate, connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties, handleSetCookieRequests); + this.contentTypePredicate = contentTypePredicate; } /* package */ CronetDataSource( CronetEngine cronetEngine, Executor executor, - @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @@ -276,7 +354,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { this.urlRequestCallback = new UrlRequestCallback(); this.cronetEngine = Assertions.checkNotNull(cronetEngine); this.executor = Assertions.checkNotNull(executor); - this.contentTypePredicate = contentTypePredicate; this.connectTimeoutMs = connectTimeoutMs; this.readTimeoutMs = readTimeoutMs; this.resetTimeoutOnRedirects = resetTimeoutOnRedirects; @@ -287,6 +364,17 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { operation = new ConditionVariable(); } + /** + * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a + * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}. + * + * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a + * predicate that was previously set. + */ + public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) { + this.contentTypePredicate = contentTypePredicate; + } + // HttpDataSource implementation. @Override @@ -363,6 +451,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } // Check for a valid content type. + Predicate contentTypePredicate = this.contentTypePredicate; if (contentTypePredicate != null) { List contentTypeHeaders = responseInfo.getAllHeaders().get(CONTENT_TYPE); String contentType = isEmpty(contentTypeHeaders) ? null : contentTypeHeaders.get(0); diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java index 93edb4e893..4086011b4f 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java @@ -20,9 +20,7 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; -import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidContentTypeException; import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.util.Predicate; import java.util.concurrent.Executor; import org.chromium.net.CronetEngine; @@ -45,8 +43,7 @@ public final class CronetDataSourceFactory extends BaseFactory { private final CronetEngineWrapper cronetEngineWrapper; private final Executor executor; - private final Predicate contentTypePredicate; - private final @Nullable TransferListener transferListener; + @Nullable private final TransferListener transferListener; private final int connectTimeoutMs; private final int readTimeoutMs; private final boolean resetTimeoutOnRedirects; @@ -64,21 +61,16 @@ public final class CronetDataSourceFactory extends BaseFactory { * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from {@link - * CronetDataSource#open}. * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no * suitable CronetEngine can be build. */ public CronetDataSourceFactory( CronetEngineWrapper cronetEngineWrapper, Executor executor, - Predicate contentTypePredicate, HttpDataSource.Factory fallbackFactory) { this( cronetEngineWrapper, executor, - contentTypePredicate, /* transferListener= */ null, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, @@ -98,20 +90,15 @@ public final class CronetDataSourceFactory extends BaseFactory { * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from {@link - * CronetDataSource#open}. * @param userAgent A user agent used to create a fallback HttpDataSource if needed. */ public CronetDataSourceFactory( CronetEngineWrapper cronetEngineWrapper, Executor executor, - Predicate contentTypePredicate, String userAgent) { this( cronetEngineWrapper, executor, - contentTypePredicate, /* transferListener= */ null, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, @@ -132,9 +119,6 @@ public final class CronetDataSourceFactory extends BaseFactory { * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from {@link - * CronetDataSource#open}. * @param connectTimeoutMs The connection timeout, in milliseconds. * @param readTimeoutMs The read timeout, in milliseconds. * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. @@ -143,7 +127,6 @@ public final class CronetDataSourceFactory extends BaseFactory { public CronetDataSourceFactory( CronetEngineWrapper cronetEngineWrapper, Executor executor, - Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @@ -151,7 +134,6 @@ public final class CronetDataSourceFactory extends BaseFactory { this( cronetEngineWrapper, executor, - contentTypePredicate, /* transferListener= */ null, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, @@ -172,9 +154,6 @@ public final class CronetDataSourceFactory extends BaseFactory { * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from {@link - * CronetDataSource#open}. * @param connectTimeoutMs The connection timeout, in milliseconds. * @param readTimeoutMs The read timeout, in milliseconds. * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. @@ -184,7 +163,6 @@ public final class CronetDataSourceFactory extends BaseFactory { public CronetDataSourceFactory( CronetEngineWrapper cronetEngineWrapper, Executor executor, - Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @@ -192,7 +170,6 @@ public final class CronetDataSourceFactory extends BaseFactory { this( cronetEngineWrapper, executor, - contentTypePredicate, /* transferListener= */ null, connectTimeoutMs, readTimeoutMs, @@ -212,9 +189,6 @@ public final class CronetDataSourceFactory extends BaseFactory { * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from {@link - * CronetDataSource#open}. * @param transferListener An optional listener. * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no * suitable CronetEngine can be build. @@ -222,11 +196,16 @@ public final class CronetDataSourceFactory extends BaseFactory { public CronetDataSourceFactory( CronetEngineWrapper cronetEngineWrapper, Executor executor, - Predicate contentTypePredicate, @Nullable TransferListener transferListener, HttpDataSource.Factory fallbackFactory) { - this(cronetEngineWrapper, executor, contentTypePredicate, transferListener, - DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory); + this( + cronetEngineWrapper, + executor, + transferListener, + DEFAULT_CONNECT_TIMEOUT_MILLIS, + DEFAULT_READ_TIMEOUT_MILLIS, + false, + fallbackFactory); } /** @@ -241,22 +220,27 @@ public final class CronetDataSourceFactory extends BaseFactory { * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from {@link - * CronetDataSource#open}. * @param transferListener An optional listener. * @param userAgent A user agent used to create a fallback HttpDataSource if needed. */ public CronetDataSourceFactory( CronetEngineWrapper cronetEngineWrapper, Executor executor, - Predicate contentTypePredicate, @Nullable TransferListener transferListener, String userAgent) { - this(cronetEngineWrapper, executor, contentTypePredicate, transferListener, - DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, - new DefaultHttpDataSourceFactory(userAgent, transferListener, - DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false)); + this( + cronetEngineWrapper, + executor, + transferListener, + DEFAULT_CONNECT_TIMEOUT_MILLIS, + DEFAULT_READ_TIMEOUT_MILLIS, + false, + new DefaultHttpDataSourceFactory( + userAgent, + transferListener, + DEFAULT_CONNECT_TIMEOUT_MILLIS, + DEFAULT_READ_TIMEOUT_MILLIS, + false)); } /** @@ -267,9 +251,6 @@ public final class CronetDataSourceFactory extends BaseFactory { * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from {@link - * CronetDataSource#open}. * @param transferListener An optional listener. * @param connectTimeoutMs The connection timeout, in milliseconds. * @param readTimeoutMs The read timeout, in milliseconds. @@ -279,16 +260,20 @@ public final class CronetDataSourceFactory extends BaseFactory { public CronetDataSourceFactory( CronetEngineWrapper cronetEngineWrapper, Executor executor, - Predicate contentTypePredicate, @Nullable TransferListener transferListener, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, String userAgent) { - this(cronetEngineWrapper, executor, contentTypePredicate, transferListener, - DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects, - new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs, - readTimeoutMs, resetTimeoutOnRedirects)); + this( + cronetEngineWrapper, + executor, + transferListener, + DEFAULT_CONNECT_TIMEOUT_MILLIS, + DEFAULT_READ_TIMEOUT_MILLIS, + resetTimeoutOnRedirects, + new DefaultHttpDataSourceFactory( + userAgent, transferListener, connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects)); } /** @@ -299,9 +284,6 @@ public final class CronetDataSourceFactory extends BaseFactory { * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from {@link - * CronetDataSource#open}. * @param transferListener An optional listener. * @param connectTimeoutMs The connection timeout, in milliseconds. * @param readTimeoutMs The read timeout, in milliseconds. @@ -312,7 +294,6 @@ public final class CronetDataSourceFactory extends BaseFactory { public CronetDataSourceFactory( CronetEngineWrapper cronetEngineWrapper, Executor executor, - Predicate contentTypePredicate, @Nullable TransferListener transferListener, int connectTimeoutMs, int readTimeoutMs, @@ -320,7 +301,6 @@ public final class CronetDataSourceFactory extends BaseFactory { HttpDataSource.Factory fallbackFactory) { this.cronetEngineWrapper = cronetEngineWrapper; this.executor = executor; - this.contentTypePredicate = contentTypePredicate; this.transferListener = transferListener; this.connectTimeoutMs = connectTimeoutMs; this.readTimeoutMs = readTimeoutMs; @@ -339,7 +319,6 @@ public final class CronetDataSourceFactory extends BaseFactory { new CronetDataSource( cronetEngine, executor, - contentTypePredicate, connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index 7c4c03dd87..df36076899 100644 --- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -38,7 +38,6 @@ import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Clock; -import com.google.android.exoplayer2.util.Predicate; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.net.SocketTimeoutException; @@ -85,7 +84,6 @@ public final class CronetDataSourceTest { @Mock private UrlRequest.Builder mockUrlRequestBuilder; @Mock private UrlRequest mockUrlRequest; - @Mock private Predicate mockContentTypePredicate; @Mock private TransferListener mockTransferListener; @Mock private Executor mockExecutor; @Mock private NetworkException mockNetworkException; @@ -95,21 +93,19 @@ public final class CronetDataSourceTest { private boolean redirectCalled; @Before - public void setUp() throws Exception { + public void setUp() { MockitoAnnotations.initMocks(this); dataSourceUnderTest = new CronetDataSource( mockCronetEngine, mockExecutor, - mockContentTypePredicate, TEST_CONNECT_TIMEOUT_MS, TEST_READ_TIMEOUT_MS, - true, // resetTimeoutOnRedirects + /* resetTimeoutOnRedirects= */ true, Clock.DEFAULT, - null, - false); + /* defaultRequestProperties= */ null, + /* handleSetCookieRequests= */ false); dataSourceUnderTest.addTransferListener(mockTransferListener); - when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true); when(mockCronetEngine.newUrlRequestBuilder( anyString(), any(UrlRequest.Callback.class), any(Executor.class))) .thenReturn(mockUrlRequestBuilder); @@ -283,7 +279,13 @@ public final class CronetDataSourceTest { @Test public void testRequestOpenValidatesContentTypePredicate() { mockResponseStartSuccess(); - when(mockContentTypePredicate.evaluate(anyString())).thenReturn(false); + + ArrayList testedContentTypes = new ArrayList<>(); + dataSourceUnderTest.setContentTypePredicate( + (String input) -> { + testedContentTypes.add(input); + return false; + }); try { dataSourceUnderTest.open(testDataSpec); @@ -292,7 +294,8 @@ public final class CronetDataSourceTest { assertThat(e instanceof HttpDataSource.InvalidContentTypeException).isTrue(); // Check for connection not automatically closed. verify(mockUrlRequest, never()).cancel(); - verify(mockContentTypePredicate).evaluate(TEST_CONTENT_TYPE); + assertThat(testedContentTypes).hasSize(1); + assertThat(testedContentTypes.get(0)).isEqualTo(TEST_CONTENT_TYPE); } } @@ -734,7 +737,6 @@ public final class CronetDataSourceTest { new CronetDataSource( mockCronetEngine, mockExecutor, - mockContentTypePredicate, TEST_CONNECT_TIMEOUT_MS, TEST_READ_TIMEOUT_MS, true, // resetTimeoutOnRedirects @@ -765,13 +767,12 @@ public final class CronetDataSourceTest { new CronetDataSource( mockCronetEngine, mockExecutor, - mockContentTypePredicate, TEST_CONNECT_TIMEOUT_MS, TEST_READ_TIMEOUT_MS, - true, // resetTimeoutOnRedirects + /* resetTimeoutOnRedirects= */ true, Clock.DEFAULT, - null, - true); + /* defaultRequestProperties= */ null, + /* handleSetCookieRequests= */ true); dataSourceUnderTest.addTransferListener(mockTransferListener); dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE); @@ -804,13 +805,12 @@ public final class CronetDataSourceTest { new CronetDataSource( mockCronetEngine, mockExecutor, - mockContentTypePredicate, TEST_CONNECT_TIMEOUT_MS, TEST_READ_TIMEOUT_MS, - true, // resetTimeoutOnRedirects + /* resetTimeoutOnRedirects= */ true, Clock.DEFAULT, - null, - true); + /* defaultRequestProperties= */ null, + /* handleSetCookieRequests= */ true); dataSourceUnderTest.addTransferListener(mockTransferListener); mockSingleRedirectSuccess(); mockFollowRedirectSuccess(); diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index 8eb8bba920..eaa305875b 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -57,14 +57,14 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { private final Call.Factory callFactory; private final RequestProperties requestProperties; - private final @Nullable String userAgent; - private final @Nullable Predicate contentTypePredicate; - private final @Nullable CacheControl cacheControl; - private final @Nullable RequestProperties defaultRequestProperties; + @Nullable private final String userAgent; + @Nullable private final CacheControl cacheControl; + @Nullable private final RequestProperties defaultRequestProperties; - private @Nullable DataSpec dataSpec; - private @Nullable Response response; - private @Nullable InputStream responseByteStream; + @Nullable private Predicate contentTypePredicate; + @Nullable private DataSpec dataSpec; + @Nullable private Response response; + @Nullable private InputStream responseByteStream; private boolean opened; private long bytesToSkip; @@ -79,7 +79,28 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { * @param userAgent An optional User-Agent string. */ public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent) { - this(callFactory, userAgent, /* contentTypePredicate= */ null); + this(callFactory, userAgent, /* cacheControl= */ null, /* defaultRequestProperties= */ null); + } + + /** + * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use + * by the source. + * @param userAgent An optional User-Agent string. + * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. + * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the + * server as HTTP headers on every request. + */ + public OkHttpDataSource( + Call.Factory callFactory, + @Nullable String userAgent, + @Nullable CacheControl cacheControl, + @Nullable RequestProperties defaultRequestProperties) { + super(/* isNetwork= */ true); + this.callFactory = Assertions.checkNotNull(callFactory); + this.userAgent = userAgent; + this.cacheControl = cacheControl; + this.defaultRequestProperties = defaultRequestProperties; + this.requestProperties = new RequestProperties(); } /** @@ -89,7 +110,10 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a {@link InvalidContentTypeException} is thrown from {@link * #open(DataSpec)}. + * @deprecated Use {@link #OkHttpDataSource(Call.Factory, String)} and {@link + * #setContentTypePredicate(Predicate)}. */ + @Deprecated public OkHttpDataSource( Call.Factory callFactory, @Nullable String userAgent, @@ -110,9 +134,12 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { * predicate then a {@link InvalidContentTypeException} is thrown from {@link * #open(DataSpec)}. * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. - * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to - * the server as HTTP headers on every request. + * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the + * server as HTTP headers on every request. + * @deprecated Use {@link #OkHttpDataSource(Call.Factory, String, CacheControl, + * RequestProperties)} and {@link #setContentTypePredicate(Predicate)}. */ + @Deprecated public OkHttpDataSource( Call.Factory callFactory, @Nullable String userAgent, @@ -128,6 +155,17 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { this.requestProperties = new RequestProperties(); } + /** + * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a + * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}. + * + * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a + * predicate that was previously set. + */ + public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) { + this.contentTypePredicate = contentTypePredicate; + } + @Override public @Nullable Uri getUri() { return response == null ? null : Uri.parse(response.request().url().toString()); diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java index d0ef35cb07..f18e37c5c4 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java @@ -89,7 +89,6 @@ public final class OkHttpDataSourceFactory extends BaseFactory { new OkHttpDataSource( callFactory, userAgent, - /* contentTypePredicate= */ null, cacheControl, defaultRequestProperties); if (listener != null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index 65b65efe2c..5955a5d9d9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -74,13 +74,13 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou private final int connectTimeoutMillis; private final int readTimeoutMillis; private final String userAgent; - private final @Nullable Predicate contentTypePredicate; - private final @Nullable RequestProperties defaultRequestProperties; + @Nullable private final RequestProperties defaultRequestProperties; private final RequestProperties requestProperties; - private @Nullable DataSpec dataSpec; - private @Nullable HttpURLConnection connection; - private @Nullable InputStream inputStream; + @Nullable private Predicate contentTypePredicate; + @Nullable private DataSpec dataSpec; + @Nullable private HttpURLConnection connection; + @Nullable private InputStream inputStream; private boolean opened; private long bytesToSkip; @@ -91,7 +91,50 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou /** @param userAgent The User-Agent string that should be used. */ public DefaultHttpDataSource(String userAgent) { - this(userAgent, /* contentTypePredicate= */ null); + this(userAgent, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS); + } + + /** + * @param userAgent The User-Agent string that should be used. + * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is + * interpreted as an infinite timeout. + * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as + * an infinite timeout. + */ + public DefaultHttpDataSource(String userAgent, int connectTimeoutMillis, int readTimeoutMillis) { + this( + userAgent, + connectTimeoutMillis, + readTimeoutMillis, + /* allowCrossProtocolRedirects= */ false, + /* defaultRequestProperties= */ null); + } + + /** + * @param userAgent The User-Agent string that should be used. + * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is + * interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use the + * default value. + * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as + * an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value. + * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP + * to HTTPS and vice versa) are enabled. + * @param defaultRequestProperties The default request properties to be sent to the server as HTTP + * headers or {@code null} if not required. + */ + public DefaultHttpDataSource( + String userAgent, + int connectTimeoutMillis, + int readTimeoutMillis, + boolean allowCrossProtocolRedirects, + @Nullable RequestProperties defaultRequestProperties) { + super(/* isNetwork= */ true); + this.userAgent = Assertions.checkNotEmpty(userAgent); + this.requestProperties = new RequestProperties(); + this.connectTimeoutMillis = connectTimeoutMillis; + this.readTimeoutMillis = readTimeoutMillis; + this.allowCrossProtocolRedirects = allowCrossProtocolRedirects; + this.defaultRequestProperties = defaultRequestProperties; } /** @@ -99,7 +142,10 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link * #open(DataSpec)}. + * @deprecated Use {@link #DefaultHttpDataSource(String)} and {@link + * #setContentTypePredicate(Predicate)}. */ + @Deprecated public DefaultHttpDataSource(String userAgent, @Nullable Predicate contentTypePredicate) { this( userAgent, @@ -117,7 +163,10 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * interpreted as an infinite timeout. * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as * an infinite timeout. + * @deprecated Use {@link #DefaultHttpDataSource(String, int, int)} and {@link + * #setContentTypePredicate(Predicate)}. */ + @Deprecated public DefaultHttpDataSource( String userAgent, @Nullable Predicate contentTypePredicate, @@ -146,7 +195,10 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * to HTTPS and vice versa) are enabled. * @param defaultRequestProperties The default request properties to be sent to the server as HTTP * headers or {@code null} if not required. + * @deprecated Use {@link #DefaultHttpDataSource(String, int, int, boolean, RequestProperties)} + * and {@link #setContentTypePredicate(Predicate)}. */ + @Deprecated public DefaultHttpDataSource( String userAgent, @Nullable Predicate contentTypePredicate, @@ -164,6 +216,17 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou this.defaultRequestProperties = defaultRequestProperties; } + /** + * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a + * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}. + * + * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a + * predicate that was previously set. + */ + public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) { + this.contentTypePredicate = contentTypePredicate; + } + @Override public @Nullable Uri getUri() { return connection == null ? null : Uri.parse(connection.getURL().toString()); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java index 371343857f..e0b1efad54 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java @@ -107,7 +107,6 @@ public final class DefaultHttpDataSourceFactory extends BaseFactory { DefaultHttpDataSource dataSource = new DefaultHttpDataSource( userAgent, - /* contentTypePredicate= */ null, connectTimeoutMillis, readTimeoutMillis, allowCrossProtocolRedirects, From 3e990a3d24e28eeaefd57f7e5bfb5b3522f05adb Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 23 May 2019 16:54:45 +0100 Subject: [PATCH 1311/1556] Fix nullness warning for MediaSource/MediaPeriod classes. PiperOrigin-RevId: 249652301 --- .../source/AbstractConcatenatedTimeline.java | 7 ++- .../source/ClippingMediaPeriod.java | 29 ++++++----- .../source/ClippingMediaSource.java | 9 ++-- .../source/DeferredMediaPeriod.java | 41 ++++++++------- .../source/ExtractorMediaSource.java | 16 +++--- .../exoplayer2/source/MergingMediaPeriod.java | 35 ++++++++----- .../exoplayer2/source/MergingMediaSource.java | 8 +-- .../source/SingleSampleMediaPeriod.java | 21 +++++--- .../source/ads/AdPlaybackState.java | 33 +++++++----- .../exoplayer2/source/ads/AdsMediaSource.java | 51 ++++++++++--------- 10 files changed, 146 insertions(+), 104 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java index 4a3505749a..db19764318 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -19,6 +19,7 @@ import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Assertions; /** * Abstract base class for the concatenation of one or more {@link Timeline}s. @@ -35,6 +36,7 @@ import com.google.android.exoplayer2.Timeline; * @param concatenatedUid UID of a period in a concatenated timeline. * @return UID of the child timeline this period belongs to. */ + @SuppressWarnings("nullness:return.type.incompatible") public static Object getChildTimelineUidFromConcatenatedUid(Object concatenatedUid) { return ((Pair) concatenatedUid).first; } @@ -45,6 +47,7 @@ import com.google.android.exoplayer2.Timeline; * @param concatenatedUid UID of a period in a concatenated timeline. * @return UID of the period in the child timeline. */ + @SuppressWarnings("nullness:return.type.incompatible") public static Object getChildPeriodUidFromConcatenatedUid(Object concatenatedUid) { return ((Pair) concatenatedUid).second; } @@ -220,7 +223,9 @@ import com.google.android.exoplayer2.Timeline; setIds); period.windowIndex += firstWindowIndexInChild; if (setIds) { - period.uid = getConcatenatedUid(getChildUidByChildIndex(childIndex), period.uid); + period.uid = + getConcatenatedUid( + getChildUidByChildIndex(childIndex), Assertions.checkNotNull(period.uid)); } return period; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index c078053110..d57dccd8fe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; @@ -25,6 +26,7 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Wraps a {@link MediaPeriod} and clips its {@link SampleStream}s to provide a subsequence of their @@ -37,8 +39,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb */ public final MediaPeriod mediaPeriod; - private MediaPeriod.Callback callback; - private ClippingSampleStream[] sampleStreams; + @Nullable private MediaPeriod.Callback callback; + private @NullableType ClippingSampleStream[] sampleStreams; private long pendingInitialDiscontinuityPositionUs; /* package */ long startUs; /* package */ long endUs; @@ -95,10 +97,14 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { sampleStreams = new ClippingSampleStream[streams.length]; - SampleStream[] childStreams = new SampleStream[streams.length]; + @NullableType SampleStream[] childStreams = new SampleStream[streams.length]; for (int i = 0; i < streams.length; i++) { sampleStreams[i] = (ClippingSampleStream) streams[i]; childStreams[i] = sampleStreams[i] != null ? sampleStreams[i].childStream : null; @@ -119,7 +125,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb for (int i = 0; i < streams.length; i++) { if (childStreams[i] == null) { sampleStreams[i] = null; - } else if (streams[i] == null || sampleStreams[i].childStream != childStreams[i]) { + } else if (sampleStreams[i] == null || sampleStreams[i].childStream != childStreams[i]) { sampleStreams[i] = new ClippingSampleStream(childStreams[i]); } streams[i] = sampleStreams[i]; @@ -209,12 +215,12 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public void onPrepared(MediaPeriod mediaPeriod) { - callback.onPrepared(this); + Assertions.checkNotNull(callback).onPrepared(this); } @Override public void onContinueLoadingRequested(MediaPeriod source) { - callback.onContinueLoadingRequested(this); + Assertions.checkNotNull(callback).onContinueLoadingRequested(this); } /* package */ boolean isPendingInitialDiscontinuity() { @@ -238,7 +244,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } } - private static boolean shouldKeepInitialDiscontinuity(long startUs, TrackSelection[] selections) { + private static boolean shouldKeepInitialDiscontinuity( + long startUs, @NullableType TrackSelection[] selections) { // If the clipping start position is non-zero, the clipping sample streams will adjust // timestamps on buffers they read from the unclipped sample streams. These adjusted buffer // timestamps can be negative, because sample streams provide buffers starting at a key-frame, @@ -300,7 +307,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } int result = childStream.readData(formatHolder, buffer, requireFormat); if (result == C.RESULT_FORMAT_READ) { - Format format = formatHolder.format; + Format format = Assertions.checkNotNull(formatHolder.format); if (format.encoderDelay != 0 || format.encoderPadding != 0) { // Clear gapless playback metadata if the start/end points don't match the media. int encoderDelay = startUs != 0 ? 0 : format.encoderDelay; @@ -328,7 +335,5 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } return childStream.skipData(positionUs); } - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index ce6254e975..c3e700fff5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; + import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -86,9 +87,9 @@ public final class ClippingMediaSource extends CompositeMediaSource { private final ArrayList mediaPeriods; private final Timeline.Window window; - private @Nullable Object manifest; - private ClippingTimeline clippingTimeline; - private IllegalClippingException clippingError; + @Nullable private Object manifest; + @Nullable private ClippingTimeline clippingTimeline; + @Nullable private IllegalClippingException clippingError; private long periodStartUs; private long periodEndUs; @@ -222,7 +223,7 @@ public final class ClippingMediaSource extends CompositeMediaSource { Assertions.checkState(mediaPeriods.remove(mediaPeriod)); mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); if (mediaPeriods.isEmpty() && !allowDynamicClippingUpdates) { - refreshClippedTimeline(clippingTimeline.timeline); + refreshClippedTimeline(Assertions.checkNotNull(clippingTimeline).timeline); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java index abf02541c8..95a218bfe7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.source; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SeekParameters; @@ -22,6 +24,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Media period that wraps a media source and defers calling its {@link @@ -47,10 +50,10 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb private final Allocator allocator; - private MediaPeriod mediaPeriod; - private Callback callback; + @Nullable private MediaPeriod mediaPeriod; + @Nullable private Callback callback; private long preparePositionUs; - private @Nullable PrepareErrorListener listener; + @Nullable private PrepareErrorListener listener; private boolean notifiedPrepareError; private long preparePositionOverrideUs; @@ -150,53 +153,57 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public TrackGroupArray getTrackGroups() { - return mediaPeriod.getTrackGroups(); + return castNonNull(mediaPeriod).getTrackGroups(); } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { if (preparePositionOverrideUs != C.TIME_UNSET && positionUs == preparePositionUs) { positionUs = preparePositionOverrideUs; preparePositionOverrideUs = C.TIME_UNSET; } - return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, - positionUs); + return castNonNull(mediaPeriod) + .selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, positionUs); } @Override public void discardBuffer(long positionUs, boolean toKeyframe) { - mediaPeriod.discardBuffer(positionUs, toKeyframe); + castNonNull(mediaPeriod).discardBuffer(positionUs, toKeyframe); } @Override public long readDiscontinuity() { - return mediaPeriod.readDiscontinuity(); + return castNonNull(mediaPeriod).readDiscontinuity(); } @Override public long getBufferedPositionUs() { - return mediaPeriod.getBufferedPositionUs(); + return castNonNull(mediaPeriod).getBufferedPositionUs(); } @Override public long seekToUs(long positionUs) { - return mediaPeriod.seekToUs(positionUs); + return castNonNull(mediaPeriod).seekToUs(positionUs); } @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - return mediaPeriod.getAdjustedSeekPositionUs(positionUs, seekParameters); + return castNonNull(mediaPeriod).getAdjustedSeekPositionUs(positionUs, seekParameters); } @Override public long getNextLoadPositionUs() { - return mediaPeriod.getNextLoadPositionUs(); + return castNonNull(mediaPeriod).getNextLoadPositionUs(); } @Override public void reevaluateBuffer(long positionUs) { - mediaPeriod.reevaluateBuffer(positionUs); + castNonNull(mediaPeriod).reevaluateBuffer(positionUs); } @Override @@ -206,14 +213,14 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public void onContinueLoadingRequested(MediaPeriod source) { - callback.onContinueLoadingRequested(this); + castNonNull(callback).onContinueLoadingRequested(this); } // MediaPeriod.Callback implementation @Override public void onPrepared(MediaPeriod mediaPeriod) { - callback.onPrepared(this); + castNonNull(callback).onPrepared(this); } private long getPreparePositionWithOverride(long preparePositionUs) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 3951dc20a2..841f18bab4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -242,8 +242,8 @@ public final class ExtractorMediaSource extends BaseMediaSource Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, - Handler eventHandler, - EventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable EventListener eventListener) { this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null); } @@ -264,9 +264,9 @@ public final class ExtractorMediaSource extends BaseMediaSource Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, - Handler eventHandler, - EventListener eventListener, - String customCacheKey) { + @Nullable Handler eventHandler, + @Nullable EventListener eventListener, + @Nullable String customCacheKey) { this( uri, dataSourceFactory, @@ -296,9 +296,9 @@ public final class ExtractorMediaSource extends BaseMediaSource Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, - Handler eventHandler, - EventListener eventListener, - String customCacheKey, + @Nullable Handler eventHandler, + @Nullable EventListener eventListener, + @Nullable String customCacheKey, int continueLoadingCheckIntervalBytes) { this( uri, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index a4fc8c6b00..cafc052f34 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -23,6 +24,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Merges multiple {@link MediaPeriod}s. @@ -35,9 +37,8 @@ import java.util.IdentityHashMap; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final ArrayList childrenPendingPreparation; - private Callback callback; - private TrackGroupArray trackGroups; - + @Nullable private Callback callback; + @Nullable private TrackGroupArray trackGroups; private MediaPeriod[] enabledPeriods; private SequenceableLoader compositeSequenceableLoader; @@ -49,6 +50,7 @@ import java.util.IdentityHashMap; compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(); streamPeriodIndices = new IdentityHashMap<>(); + enabledPeriods = new MediaPeriod[0]; } @Override @@ -69,12 +71,16 @@ import java.util.IdentityHashMap; @Override public TrackGroupArray getTrackGroups() { - return trackGroups; + return Assertions.checkNotNull(trackGroups); } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { // Map each selection and stream onto a child period index. int[] streamChildIndices = new int[selections.length]; int[] selectionChildIndices = new int[selections.length]; @@ -94,9 +100,9 @@ import java.util.IdentityHashMap; } streamPeriodIndices.clear(); // Select tracks for each child, copying the resulting streams back into a new streams array. - SampleStream[] newStreams = new SampleStream[selections.length]; - SampleStream[] childStreams = new SampleStream[selections.length]; - TrackSelection[] childSelections = new TrackSelection[selections.length]; + @NullableType SampleStream[] newStreams = new SampleStream[selections.length]; + @NullableType SampleStream[] childStreams = new SampleStream[selections.length]; + @NullableType TrackSelection[] childSelections = new TrackSelection[selections.length]; ArrayList enabledPeriodsList = new ArrayList<>(periods.length); for (int i = 0; i < periods.length; i++) { for (int j = 0; j < selections.length; j++) { @@ -114,10 +120,10 @@ import java.util.IdentityHashMap; for (int j = 0; j < selections.length; j++) { if (selectionChildIndices[j] == i) { // Assert that the child provided a stream for the selection. - Assertions.checkState(childStreams[j] != null); + SampleStream childStream = Assertions.checkNotNull(childStreams[j]); newStreams[j] = childStreams[j]; periodEnabled = true; - streamPeriodIndices.put(childStreams[j], i); + streamPeriodIndices.put(childStream, i); } else if (streamChildIndices[j] == i) { // Assert that the child cleared any previous stream. Assertions.checkState(childStreams[j] == null); @@ -208,7 +214,8 @@ import java.util.IdentityHashMap; @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - return enabledPeriods[0].getAdjustedSeekPositionUs(positionUs, seekParameters); + MediaPeriod queryPeriod = enabledPeriods.length > 0 ? enabledPeriods[0] : periods[0]; + return queryPeriod.getAdjustedSeekPositionUs(positionUs, seekParameters); } // MediaPeriod.Callback implementation @@ -233,12 +240,12 @@ import java.util.IdentityHashMap; } } trackGroups = new TrackGroupArray(trackGroupArray); - callback.onPrepared(this); + Assertions.checkNotNull(callback).onPrepared(this); } @Override public void onContinueLoadingRequested(MediaPeriod ignored) { - callback.onContinueLoadingRequested(this); + Assertions.checkNotNull(callback).onContinueLoadingRequested(this); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index 6b1a362b59..7188cada0f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -71,9 +71,9 @@ public final class MergingMediaSource extends CompositeMediaSource { private final ArrayList pendingTimelineSources; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private Object primaryManifest; + @Nullable private Object primaryManifest; private int periodCount; - private IllegalMergeException mergeError; + @Nullable private IllegalMergeException mergeError; /** * @param mediaSources The {@link MediaSource}s to merge. @@ -170,11 +170,13 @@ public final class MergingMediaSource extends CompositeMediaSource { } @Override - protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + @Nullable + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( Integer id, MediaPeriodId mediaPeriodId) { return id == 0 ? mediaPeriodId : null; } + @Nullable private IllegalMergeException checkTimelineMerges(Timeline timeline) { if (periodCount == PERIOD_COUNT_UNSET) { periodCount = timeline.getPeriodCount(); 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 e0c2a00df3..6063168e99 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 @@ -31,11 +31,14 @@ import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; import com.google.android.exoplayer2.upstream.Loader.Loadable; import com.google.android.exoplayer2.upstream.StatsDataSource; import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link MediaPeriod} with a single sample. @@ -64,8 +67,7 @@ import java.util.Arrays; /* package */ boolean notifiedReadingStarted; /* package */ boolean loadingFinished; - /* package */ boolean loadingSucceeded; - /* package */ byte[] sampleData; + /* package */ byte @MonotonicNonNull [] sampleData; /* package */ int sampleSize; public SingleSampleMediaPeriod( @@ -112,8 +114,12 @@ import java.util.Arrays; } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { for (int i = 0; i < selections.length; i++) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { sampleStreams.remove(streams[i]); @@ -204,9 +210,8 @@ import java.util.Arrays; public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { sampleSize = (int) loadable.dataSource.getBytesRead(); - sampleData = loadable.sampleData; + sampleData = Assertions.checkNotNull(loadable.sampleData); loadingFinished = true; - loadingSucceeded = true; eventDispatcher.loadCompleted( loadable.dataSpec, loadable.dataSource.getLastOpenedUri(), @@ -325,7 +330,7 @@ import java.util.Arrays; streamState = STREAM_STATE_SEND_SAMPLE; return C.RESULT_FORMAT_READ; } else if (loadingFinished) { - if (loadingSucceeded) { + if (sampleData != null) { buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); buffer.timeUs = 0; if (buffer.isFlagsOnly()) { @@ -371,7 +376,7 @@ import java.util.Arrays; private final StatsDataSource dataSource; - private byte[] sampleData; + @Nullable private byte[] sampleData; public SourceLoadable(DataSpec dataSpec, DataSource dataSource) { this.dataSpec = dataSpec; 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 be9dea91f1..0a1628b3f9 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 @@ -18,12 +18,15 @@ package com.google.android.exoplayer2.source.ads; import android.net.Uri; import androidx.annotation.CheckResult; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Represents ad group times relative to the start of the media and information on the state and @@ -45,9 +48,9 @@ public final class AdPlaybackState { /** The number of ads in the ad group, or {@link C#LENGTH_UNSET} if unknown. */ public final int count; /** The URI of each ad in the ad group. */ - public final Uri[] uris; + public final @NullableType Uri[] uris; /** The state of each ad in the ad group. */ - public final @AdState int[] states; + @AdState public final int[] states; /** The durations of each ad in the ad group, in microseconds. */ public final long[] durationsUs; @@ -60,7 +63,8 @@ public final class AdPlaybackState { /* durationsUs= */ new long[0]); } - private AdGroup(int count, @AdState int[] states, Uri[] uris, long[] durationsUs) { + private AdGroup( + int count, @AdState int[] states, @NullableType Uri[] uris, long[] durationsUs) { Assertions.checkArgument(states.length == uris.length); this.count = count; this.states = states; @@ -98,7 +102,7 @@ public final class AdPlaybackState { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } @@ -130,7 +134,7 @@ public final class AdPlaybackState { Assertions.checkArgument(this.count == C.LENGTH_UNSET && states.length <= count); @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, count); long[] durationsUs = copyDurationsUsWithSpaceForAdCount(this.durationsUs, count); - Uri[] uris = Arrays.copyOf(this.uris, count); + @NullableType Uri[] uris = Arrays.copyOf(this.uris, count); return new AdGroup(count, states, uris, durationsUs); } @@ -151,7 +155,7 @@ public final class AdPlaybackState { this.durationsUs.length == states.length ? this.durationsUs : copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length); - Uri[] uris = Arrays.copyOf(this.uris, states.length); + @NullableType Uri[] uris = Arrays.copyOf(this.uris, states.length); uris[index] = uri; states[index] = AD_STATE_AVAILABLE; return new AdGroup(count, states, uris, durationsUs); @@ -177,6 +181,7 @@ public final class AdPlaybackState { this.durationsUs.length == states.length ? this.durationsUs : copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length); + @NullableType Uri[] uris = this.uris.length == states.length ? this.uris : Arrays.copyOf(this.uris, states.length); states[index] = state; @@ -362,7 +367,7 @@ public final class AdPlaybackState { if (adGroups[adGroupIndex].count == adCount) { return this; } - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = this.adGroups[adGroupIndex].withAdCount(adCount); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -370,7 +375,7 @@ public final class AdPlaybackState { /** Returns an instance with the specified ad URI. */ @CheckResult public AdPlaybackState withAdUri(int adGroupIndex, int adIndexInAdGroup, Uri uri) { - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdUri(uri, adIndexInAdGroup); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -378,7 +383,7 @@ public final class AdPlaybackState { /** Returns an instance with the specified ad marked as played. */ @CheckResult public AdPlaybackState withPlayedAd(int adGroupIndex, int adIndexInAdGroup) { - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_PLAYED, adIndexInAdGroup); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -386,7 +391,7 @@ public final class AdPlaybackState { /** 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); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_SKIPPED, adIndexInAdGroup); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -394,7 +399,7 @@ public final class AdPlaybackState { /** Returns an instance with the specified ad marked as having a load error. */ @CheckResult public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) { - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_ERROR, adIndexInAdGroup); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -405,7 +410,7 @@ public final class AdPlaybackState { */ @CheckResult public AdPlaybackState withSkippedAdGroup(int adGroupIndex) { - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = adGroups[adGroupIndex].withAllAdsSkipped(); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -413,7 +418,7 @@ public final class AdPlaybackState { /** Returns an instance with the specified ad durations, in microseconds. */ @CheckResult public AdPlaybackState withAdDurationsUs(long[][] adDurationUs) { - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); for (int adGroupIndex = 0; adGroupIndex < adGroupCount; adGroupIndex++) { adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdDurationsUs(adDurationUs[adGroupIndex]); } @@ -441,7 +446,7 @@ public final class AdPlaybackState { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 8828e34304..78b0f6de11 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -47,6 +47,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A {@link MediaSource} that inserts ads linearly with a provided content media source. This source @@ -114,7 +115,7 @@ public final class AdsMediaSource extends CompositeMediaSource { */ public RuntimeException getRuntimeExceptionForUnexpected() { Assertions.checkState(type == TYPE_UNEXPECTED); - return (RuntimeException) getCause(); + return (RuntimeException) Assertions.checkNotNull(getCause()); } } @@ -131,12 +132,12 @@ public final class AdsMediaSource extends CompositeMediaSource { private final Timeline.Period period; // Accessed on the player thread. - private ComponentListener componentListener; - private Timeline contentTimeline; - private Object contentManifest; - private AdPlaybackState adPlaybackState; - private MediaSource[][] adGroupMediaSources; - private Timeline[][] adGroupTimelines; + @Nullable private ComponentListener componentListener; + @Nullable private Timeline contentTimeline; + @Nullable private Object contentManifest; + @Nullable private AdPlaybackState adPlaybackState; + private @NullableType MediaSource[][] adGroupMediaSources; + private @NullableType Timeline[][] adGroupTimelines; /** * Constructs a new source that inserts ads linearly with the content specified by {@code @@ -202,24 +203,25 @@ public final class AdsMediaSource extends CompositeMediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + AdPlaybackState adPlaybackState = Assertions.checkNotNull(this.adPlaybackState); if (adPlaybackState.adGroupCount > 0 && id.isAd()) { int adGroupIndex = id.adGroupIndex; int adIndexInAdGroup = id.adIndexInAdGroup; - Uri adUri = adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup]; + Uri adUri = + Assertions.checkNotNull(adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup]); if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { - MediaSource adMediaSource = adMediaSourceFactory.createMediaSource(adUri); - int oldAdCount = adGroupMediaSources[adGroupIndex].length; - if (adIndexInAdGroup >= oldAdCount) { - int adCount = adIndexInAdGroup + 1; - adGroupMediaSources[adGroupIndex] = - Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount); - adGroupTimelines[adGroupIndex] = Arrays.copyOf(adGroupTimelines[adGroupIndex], adCount); - } - adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource; - deferredMediaPeriodByAdMediaSource.put(adMediaSource, new ArrayList<>()); - prepareChildSource(id, adMediaSource); + int adCount = adIndexInAdGroup + 1; + adGroupMediaSources[adGroupIndex] = + Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount); + adGroupTimelines[adGroupIndex] = Arrays.copyOf(adGroupTimelines[adGroupIndex], adCount); } MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; + if (mediaSource == null) { + mediaSource = adMediaSourceFactory.createMediaSource(adUri); + adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = mediaSource; + deferredMediaPeriodByAdMediaSource.put(mediaSource, new ArrayList<>()); + prepareChildSource(id, mediaSource); + } DeferredMediaPeriod deferredMediaPeriod = new DeferredMediaPeriod(mediaSource, id, allocator, startPositionUs); deferredMediaPeriod.setPrepareErrorListener( @@ -227,7 +229,8 @@ public final class AdsMediaSource extends CompositeMediaSource { List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); if (mediaPeriods == null) { Object periodUid = - adGroupTimelines[adGroupIndex][adIndexInAdGroup].getUidOfPeriod(/* periodIndex= */ 0); + Assertions.checkNotNull(adGroupTimelines[adGroupIndex][adIndexInAdGroup]) + .getUidOfPeriod(/* periodIndex= */ 0); MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, id.windowSequenceNumber); deferredMediaPeriod.createPeriod(adSourceMediaPeriodId); } else { @@ -258,7 +261,7 @@ public final class AdsMediaSource extends CompositeMediaSource { @Override public void releaseSourceInternal() { super.releaseSourceInternal(); - componentListener.release(); + Assertions.checkNotNull(componentListener).release(); componentListener = null; deferredMediaPeriodByAdMediaSource.clear(); contentTimeline = null; @@ -305,7 +308,7 @@ public final class AdsMediaSource extends CompositeMediaSource { maybeUpdateSourceInfo(); } - private void onContentSourceInfoRefreshed(Timeline timeline, Object manifest) { + private void onContentSourceInfoRefreshed(Timeline timeline, @Nullable Object manifest) { Assertions.checkArgument(timeline.getPeriodCount() == 1); contentTimeline = timeline; contentManifest = manifest; @@ -330,6 +333,7 @@ public final class AdsMediaSource extends CompositeMediaSource { } private void maybeUpdateSourceInfo() { + Timeline contentTimeline = this.contentTimeline; if (adPlaybackState != null && contentTimeline != null) { adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurations(adGroupTimelines, period)); Timeline timeline = @@ -340,7 +344,8 @@ public final class AdsMediaSource extends CompositeMediaSource { } } - private static long[][] getAdDurations(Timeline[][] adTimelines, Timeline.Period period) { + private static long[][] getAdDurations( + @NullableType Timeline[][] adTimelines, Timeline.Period period) { long[][] adDurations = new long[adTimelines.length][]; for (int i = 0; i < adTimelines.length; i++) { adDurations[i] = new long[adTimelines[i].length]; From 11c0c6d2662c5a7c04ae61f436d1dcbdd655a5b9 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 24 May 2019 13:53:22 +0100 Subject: [PATCH 1312/1556] Reset upstream format when empty track selection happens PiperOrigin-RevId: 249819080 --- .../android/exoplayer2/source/hls/HlsSampleStreamWrapper.java | 1 + 1 file changed, 1 insertion(+) 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 65039b9364..434b6c2011 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 @@ -322,6 +322,7 @@ import java.util.Map; if (enabledTrackGroupCount == 0) { chunkSource.reset(); downstreamTrackFormat = null; + pendingResetUpstreamFormats = true; mediaChunks.clear(); if (loader.isLoading()) { if (sampleQueuesBuilt) { From 3afdd7ac5ab48284a481e6bdabe562e3a2d814b0 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 24 May 2019 15:19:05 +0100 Subject: [PATCH 1313/1556] Put @Nullable annotation in the right place PiperOrigin-RevId: 249828748 --- .../exoplayer2/demo/PlayerActivity.java | 3 ++- .../exoplayer2/ext/ffmpeg/FfmpegDecoder.java | 2 +- .../ext/flac/FlacExtractorSeekTest.java | 3 ++- .../exoplayer2/ext/flac/FlacExtractor.java | 2 +- .../exoplayer2/ext/ima/ImaAdsLoader.java | 8 +++---- .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 3 ++- .../ext/leanback/LeanbackPlayerAdapter.java | 6 ++--- .../ext/okhttp/OkHttpDataSource.java | 3 ++- .../ext/okhttp/OkHttpDataSourceFactory.java | 6 ++--- .../ext/rtmp/RtmpDataSourceFactory.java | 2 +- .../android/exoplayer2/DefaultMediaClock.java | 4 ++-- .../android/exoplayer2/ExoPlayerImpl.java | 5 +++-- .../exoplayer2/ExoPlayerImplInternal.java | 2 +- .../com/google/android/exoplayer2/Format.java | 20 ++++++++--------- .../android/exoplayer2/MediaPeriodQueue.java | 8 +++---- .../android/exoplayer2/PlaybackInfo.java | 2 +- .../android/exoplayer2/PlayerMessage.java | 5 +++-- .../android/exoplayer2/SimpleExoPlayer.java | 3 ++- .../analytics/AnalyticsCollector.java | 22 ++++++++++++------- .../analytics/AnalyticsListener.java | 2 +- .../exoplayer2/audio/AudioFocusManager.java | 2 +- .../audio/AudioTimestampPoller.java | 2 +- .../audio/AudioTrackPositionTracker.java | 6 ++--- .../exoplayer2/drm/DefaultDrmSession.java | 14 +++++++----- .../drm/DefaultDrmSessionManager.java | 6 ++--- .../android/exoplayer2/drm/DrmInitData.java | 9 ++++---- .../exoplayer2/drm/ErrorStateDrmSession.java | 12 ++++++---- .../extractor/amr/AmrExtractor.java | 2 +- .../exoplayer2/extractor/mp3/XingSeeker.java | 2 +- .../exoplayer2/extractor/mp4/Atom.java | 6 +++-- .../extractor/mp4/FragmentedMp4Extractor.java | 8 +++---- .../extractor/ts/AdtsExtractor.java | 2 +- .../mediacodec/MediaCodecRenderer.java | 2 +- .../mediacodec/MediaCodecSelector.java | 3 ++- .../exoplayer2/metadata/MetadataRenderer.java | 2 +- .../exoplayer2/metadata/id3/ApicFrame.java | 2 +- .../exoplayer2/metadata/id3/Id3Decoder.java | 8 ++++--- .../metadata/id3/TextInformationFrame.java | 2 +- .../exoplayer2/metadata/id3/UrlLinkFrame.java | 2 +- .../source/CompositeMediaSource.java | 4 ++-- .../source/ExtractorMediaSource.java | 6 ++--- .../source/MediaSourceEventListener.java | 6 ++--- .../source/ProgressiveMediaPeriod.java | 2 +- .../source/SinglePeriodTimeline.java | 2 +- .../source/SingleSampleMediaPeriod.java | 2 +- .../source/SingleSampleMediaSource.java | 4 ++-- .../exoplayer2/source/chunk/Chunk.java | 2 +- .../source/chunk/ChunkSampleStream.java | 2 +- .../android/exoplayer2/text/TextRenderer.java | 2 +- .../AdaptiveTrackSelection.java | 5 +++-- .../trackselection/FixedTrackSelection.java | 7 +++--- .../trackselection/MappingTrackSelector.java | 2 +- .../trackselection/RandomTrackSelection.java | 3 ++- .../trackselection/TrackSelectionArray.java | 3 ++- .../trackselection/TrackSelector.java | 4 ++-- .../exoplayer2/upstream/BaseDataSource.java | 2 +- .../android/exoplayer2/upstream/DataSpec.java | 6 ++--- .../upstream/DefaultDataSource.java | 17 +++++++------- .../upstream/DefaultDataSourceFactory.java | 2 +- .../upstream/DefaultHttpDataSource.java | 3 ++- .../DefaultHttpDataSourceFactory.java | 2 +- .../upstream/FileDataSourceFactory.java | 2 +- .../android/exoplayer2/upstream/Loader.java | 2 +- .../upstream/PriorityDataSource.java | 3 ++- .../exoplayer2/upstream/StatsDataSource.java | 3 ++- .../exoplayer2/upstream/TeeDataSource.java | 3 ++- .../upstream/cache/CacheDataSource.java | 15 +++++++------ .../exoplayer2/upstream/cache/CacheSpan.java | 6 ++--- .../upstream/crypto/AesCipherDataSource.java | 3 ++- .../exoplayer2/util/EGLSurfaceTexture.java | 10 ++++----- .../android/exoplayer2/util/EventLogger.java | 2 +- .../exoplayer2/util/ParsableByteArray.java | 6 +++-- .../exoplayer2/util/TimedValueQueue.java | 3 ++- .../android/exoplayer2/video/ColorInfo.java | 2 +- .../exoplayer2/video/DummySurface.java | 6 ++--- .../android/exoplayer2/video/HevcConfig.java | 2 +- .../video/MediaCodecVideoRenderer.java | 2 +- .../video/spherical/CameraMotionRenderer.java | 2 +- .../analytics/AnalyticsCollectorTest.java | 2 +- .../upstream/BaseDataSourceTest.java | 3 ++- .../source/dash/DashMediaPeriod.java | 4 ++-- .../source/dash/DashMediaSource.java | 6 ++--- .../source/dash/DefaultDashChunkSource.java | 2 +- .../source/dash/manifest/RangedUri.java | 3 ++- .../source/hls/Aes128DataSource.java | 2 +- .../exoplayer2/source/hls/HlsMediaPeriod.java | 4 ++-- .../exoplayer2/source/hls/HlsMediaSource.java | 4 ++-- .../playlist/DefaultHlsPlaylistTracker.java | 3 ++- .../source/smoothstreaming/SsMediaPeriod.java | 4 ++-- .../source/smoothstreaming/SsMediaSource.java | 4 ++-- .../android/exoplayer2/ui/DefaultTimeBar.java | 6 ++--- .../android/exoplayer2/ui/PlayerView.java | 3 ++- .../ui/spherical/ProjectionRenderer.java | 4 ++-- .../ui/spherical/SceneRenderer.java | 2 +- .../ui/spherical/SphericalSurfaceView.java | 8 +++---- .../exoplayer2/ui/spherical/TouchTracker.java | 2 +- .../android/exoplayer2/testutil/Action.java | 4 ++-- .../exoplayer2/testutil/ActionSchedule.java | 2 +- .../testutil/ExoPlayerTestRunner.java | 6 ++--- .../testutil/FakeAdaptiveMediaPeriod.java | 2 +- .../exoplayer2/testutil/FakeDataSet.java | 6 ++--- .../exoplayer2/testutil/FakeMediaSource.java | 2 +- .../exoplayer2/testutil/FakeSampleStream.java | 2 +- 103 files changed, 247 insertions(+), 206 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 8ee9e9f9f6..f7db8c7ca3 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 @@ -555,7 +555,8 @@ public class PlayerActivity extends AppCompatActivity } /** Returns an ads media source, reusing the ads loader if one exists. */ - private @Nullable MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) { + @Nullable + private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) { // Load the extension source using reflection so the demo app doesn't have to depend on it. // The ads loader is reused for multiple playbacks, so that ad playback can resume. try { diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java index 7c5864420a..12c26ca2ec 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java @@ -42,7 +42,7 @@ import java.util.List; private static final int DECODER_ERROR_OTHER = -2; private final String codecName; - private final @Nullable byte[] extraData; + @Nullable private final byte[] extraData; private final @C.Encoding int encoding; private final int outputBufferSize; diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java index 6008d99448..3beb4d0103 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java @@ -228,7 +228,8 @@ public final class FlacExtractorSeekTest { } } - private @Nullable SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output) + @Nullable + private SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output) throws IOException, InterruptedException { try { ExtractorInput input = getExtractorInputFromPosition(0); diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index bb72e114fe..79350e6ae3 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -88,7 +88,7 @@ public final class FlacExtractor implements Extractor { private FlacStreamInfo streamInfo; private Metadata id3Metadata; - private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker; + @Nullable private FlacBinarySearchSeeker flacBinarySearchSeeker; private boolean readPastStreamInfo; 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 bdeebec44c..1cdbac56b5 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 @@ -313,14 +313,14 @@ public final class ImaAdsLoader */ private static final int IMA_AD_STATE_PAUSED = 2; - private final @Nullable Uri adTagUri; - private final @Nullable String adsResponse; + @Nullable private final Uri adTagUri; + @Nullable private final String adsResponse; private final int vastLoadTimeoutMs; private final int mediaLoadTimeoutMs; private final boolean focusSkipButtonWhenAvailable; private final int mediaBitrate; - private final @Nullable Set adUiElements; - private final @Nullable AdEventListener adEventListener; + @Nullable private final Set adUiElements; + @Nullable private final AdEventListener adEventListener; private final ImaFactory imaFactory; private final Timeline.Period period; private final List adCallbacks; diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java index 1e1935c63a..ab880703ee 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -252,7 +252,8 @@ public class ImaAdsLoaderTest { } @Override - public @Nullable Ad getAd() { + @Nullable + public Ad getAd() { return ad; } 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 5705b73ab2..1fece6bc8e 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 @@ -51,10 +51,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab private final ComponentListener componentListener; private final int updatePeriodMs; - private @Nullable PlaybackPreparer playbackPreparer; + @Nullable private PlaybackPreparer playbackPreparer; private ControlDispatcher controlDispatcher; - private @Nullable ErrorMessageProvider errorMessageProvider; - private @Nullable SurfaceHolderGlueHost surfaceHolderGlueHost; + @Nullable private ErrorMessageProvider errorMessageProvider; + @Nullable private SurfaceHolderGlueHost surfaceHolderGlueHost; private boolean hasSurface; private boolean lastNotifiedPreparedState; diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index eaa305875b..ec05c52f44 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -167,7 +167,8 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return response == null ? null : Uri.parse(response.request().url().toString()); } diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java index f18e37c5c4..f3d74f9233 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java @@ -29,9 +29,9 @@ import okhttp3.Call; public final class OkHttpDataSourceFactory extends BaseFactory { private final Call.Factory callFactory; - private final @Nullable String userAgent; - private final @Nullable TransferListener listener; - private final @Nullable CacheControl cacheControl; + @Nullable private final String userAgent; + @Nullable private final TransferListener listener; + @Nullable private final CacheControl cacheControl; /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java index 36abf825d6..505724e846 100644 --- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java +++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java @@ -25,7 +25,7 @@ import com.google.android.exoplayer2.upstream.TransferListener; */ public final class RtmpDataSourceFactory implements DataSource.Factory { - private final @Nullable TransferListener listener; + @Nullable private final TransferListener listener; public RtmpDataSourceFactory() { this(null); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java index 89e7d857c8..bcec6426d6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java @@ -43,8 +43,8 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; private final StandaloneMediaClock standaloneMediaClock; private final PlaybackParameterListener listener; - private @Nullable Renderer rendererClockSource; - private @Nullable MediaClock rendererClock; + @Nullable private Renderer rendererClockSource; + @Nullable private MediaClock rendererClock; /** * Creates a new instance with listener for playback parameter changes and a {@link Clock} to use 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 bea7af189a..de6be33686 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 @@ -73,7 +73,7 @@ import java.util.concurrent.CopyOnWriteArrayList; private boolean foregroundMode; private PlaybackParameters playbackParameters; private SeekParameters seekParameters; - private @Nullable ExoPlaybackException playbackError; + @Nullable private ExoPlaybackException playbackError; // Playback information when there is no pending seek/set source operation. private PlaybackInfo playbackInfo; @@ -199,7 +199,8 @@ import java.util.concurrent.CopyOnWriteArrayList; } @Override - public @Nullable ExoPlaybackException getPlaybackError() { + @Nullable + public ExoPlaybackException getPlaybackError() { return playbackError; } 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 34d8d0aa08..ff94567a48 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 @@ -1836,7 +1836,7 @@ import java.util.concurrent.atomic.AtomicBoolean; public int resolvedPeriodIndex; public long resolvedPeriodTimeUs; - public @Nullable Object resolvedPeriodUid; + @Nullable public Object resolvedPeriodUid; public PendingMessageInfo(PlayerMessage message) { this.message = message; 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 dcb7a83dca..cf1c6f4e5a 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 @@ -45,9 +45,9 @@ public final class Format implements Parcelable { public static final long OFFSET_SAMPLE_RELATIVE = Long.MAX_VALUE; /** An identifier for the format, or null if unknown or not applicable. */ - public final @Nullable String id; + @Nullable public final String id; /** The human readable label, or null if unknown or not applicable. */ - public final @Nullable String label; + @Nullable public final String label; /** Track selection flags. */ @C.SelectionFlags public final int selectionFlags; /** Track role flags. */ @@ -57,14 +57,14 @@ public final class Format implements Parcelable { */ public final int bitrate; /** Codecs of the format as described in RFC 6381, or null if unknown or not applicable. */ - public final @Nullable String codecs; + @Nullable public final String codecs; /** Metadata, or null if unknown or not applicable. */ - public final @Nullable Metadata metadata; + @Nullable public final Metadata metadata; // Container specific. /** The mime type of the container, or null if unknown or not applicable. */ - public final @Nullable String containerMimeType; + @Nullable public final String containerMimeType; // Elementary stream specific. @@ -72,7 +72,7 @@ public final class Format implements Parcelable { * The mime type of the elementary stream (i.e. the individual samples), or null if unknown or not * applicable. */ - public final @Nullable String sampleMimeType; + @Nullable public final String sampleMimeType; /** * The maximum size of a buffer of data (typically one sample), or {@link #NO_VALUE} if unknown or * not applicable. @@ -84,7 +84,7 @@ public final class Format implements Parcelable { */ public final List initializationData; /** DRM initialization data if the stream is protected, or null otherwise. */ - public final @Nullable DrmInitData drmInitData; + @Nullable public final DrmInitData drmInitData; /** * For samples that contain subsamples, this is an offset that should be added to subsample @@ -122,9 +122,9 @@ public final class Format implements Parcelable { @C.StereoMode public final int stereoMode; /** The projection data for 360/VR video, or null if not applicable. */ - public final @Nullable byte[] projectionData; + @Nullable public final byte[] projectionData; /** The color metadata associated with the video, helps with accurate color reproduction. */ - public final @Nullable ColorInfo colorInfo; + @Nullable public final ColorInfo colorInfo; // Audio specific. @@ -157,7 +157,7 @@ public final class Format implements Parcelable { // Audio and text specific. /** The language as an IETF BCP 47 conformant tag, or null if unknown or not applicable. */ - public final @Nullable String language; + @Nullable public final String language; /** * The Accessibility channel, or {@link #NO_VALUE} if not known or applicable. */ 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 249548340e..58000d6f58 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 @@ -46,11 +46,11 @@ import com.google.android.exoplayer2.util.Assertions; private Timeline timeline; private @RepeatMode int repeatMode; private boolean shuffleModeEnabled; - private @Nullable MediaPeriodHolder playing; - private @Nullable MediaPeriodHolder reading; - private @Nullable MediaPeriodHolder loading; + @Nullable private MediaPeriodHolder playing; + @Nullable private MediaPeriodHolder reading; + @Nullable private MediaPeriodHolder loading; private int length; - private @Nullable Object oldFrontPeriodUid; + @Nullable private Object oldFrontPeriodUid; private long oldFrontPeriodWindowSequenceNumber; /** Creates a new media period queue. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index cf4643c5da..d3e4a0e626 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -36,7 +36,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /** The current {@link Timeline}. */ public final Timeline timeline; /** The current manifest. */ - public final @Nullable Object manifest; + @Nullable public final Object manifest; /** The {@link MediaPeriodId} of the currently playing media period in the {@link #timeline}. */ public final MediaPeriodId periodId; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java index 7904942c1b..49309181a0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -55,7 +55,7 @@ public final class PlayerMessage { private final Timeline timeline; private int type; - private @Nullable Object payload; + @Nullable private Object payload; private Handler handler; private int windowIndex; private long positionMs; @@ -134,7 +134,8 @@ public final class PlayerMessage { } /** Returns the message payload forwarded to {@link Target#handleMessage(int, Object)}. */ - public @Nullable Object getPayload() { + @Nullable + public Object getPayload() { return payload; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 056038d97a..da66f3dd10 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -880,7 +880,8 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public @Nullable ExoPlaybackException getPlaybackError() { + @Nullable + public ExoPlaybackException getPlaybackError() { verifyApplicationThread(); return player.getPlaybackError(); } 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 094024bc36..deecfb15a8 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 @@ -687,8 +687,8 @@ public class AnalyticsCollector private final HashMap mediaPeriodIdToInfo; private final Period period; - private @Nullable MediaPeriodInfo lastReportedPlayingMediaPeriod; - private @Nullable MediaPeriodInfo readingMediaPeriod; + @Nullable private MediaPeriodInfo lastReportedPlayingMediaPeriod; + @Nullable private MediaPeriodInfo readingMediaPeriod; private Timeline timeline; private boolean isSeeking; @@ -706,7 +706,8 @@ public class AnalyticsCollector * always return null to reflect the uncertainty about the current playing period. May also be * null, if the timeline is empty or no media period is active yet. */ - public @Nullable MediaPeriodInfo getPlayingMediaPeriod() { + @Nullable + public MediaPeriodInfo getPlayingMediaPeriod() { return mediaPeriodInfoQueue.isEmpty() || timeline.isEmpty() || isSeeking ? null : mediaPeriodInfoQueue.get(0); @@ -719,7 +720,8 @@ public class AnalyticsCollector * reported until the seek or preparation is processed. May be null, if no media period is * active yet. */ - public @Nullable MediaPeriodInfo getLastReportedPlayingMediaPeriod() { + @Nullable + public MediaPeriodInfo getLastReportedPlayingMediaPeriod() { return lastReportedPlayingMediaPeriod; } @@ -727,7 +729,8 @@ public class AnalyticsCollector * Returns the {@link MediaPeriodInfo} of the media period currently being read by the player. * May be null, if the player is not reading a media period. */ - public @Nullable MediaPeriodInfo getReadingMediaPeriod() { + @Nullable + public MediaPeriodInfo getReadingMediaPeriod() { return readingMediaPeriod; } @@ -736,14 +739,16 @@ public class AnalyticsCollector * currently loading or will be the next one loading. May be null, if no media period is active * yet. */ - public @Nullable MediaPeriodInfo getLoadingMediaPeriod() { + @Nullable + public MediaPeriodInfo getLoadingMediaPeriod() { return mediaPeriodInfoQueue.isEmpty() ? null : mediaPeriodInfoQueue.get(mediaPeriodInfoQueue.size() - 1); } /** Returns the {@link MediaPeriodInfo} for the given {@link MediaPeriodId}. */ - public @Nullable MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId mediaPeriodId) { + @Nullable + public MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId mediaPeriodId) { return mediaPeriodIdToInfo.get(mediaPeriodId); } @@ -756,7 +761,8 @@ public class AnalyticsCollector * Tries to find an existing media period info from the specified window index. Only returns a * non-null media period info if there is a unique, unambiguous match. */ - public @Nullable MediaPeriodInfo tryResolveWindowIndex(int windowIndex) { + @Nullable + public MediaPeriodInfo tryResolveWindowIndex(int windowIndex) { MediaPeriodInfo match = null; for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) { MediaPeriodInfo info = mediaPeriodInfoQueue.get(i); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java index 48578d8853..be62ad99d2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -68,7 +68,7 @@ public interface AnalyticsListener { * Media period identifier for the media period this event belongs to, or {@code null} if the * event is not associated with a specific media period. */ - public final @Nullable MediaPeriodId mediaPeriodId; + @Nullable public final MediaPeriodId mediaPeriodId; /** * Position in the window or ad this event belongs to at the time of the event, in milliseconds. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java index 3cc05e87df..2d65b64f36 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java @@ -103,7 +103,7 @@ public final class AudioFocusManager { private final AudioManager audioManager; private final AudioFocusListener focusListener; private final PlayerControl playerControl; - private @Nullable AudioAttributes audioAttributes; + @Nullable private AudioAttributes audioAttributes; private @AudioFocusState int audioFocusState; private int focusGain; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java index d43972d7b0..0564591f1f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java @@ -82,7 +82,7 @@ import java.lang.annotation.RetentionPolicy; */ private static final int INITIALIZING_DURATION_US = 500_000; - private final @Nullable AudioTimestampV19 audioTimestamp; + @Nullable private final AudioTimestampV19 audioTimestamp; private @State int state; private long initializeSystemTimeUs; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java index e87e49d2da..4ee70bd813 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java @@ -133,10 +133,10 @@ import java.lang.reflect.Method; private final Listener listener; private final long[] playheadOffsets; - private @Nullable AudioTrack audioTrack; + @Nullable private AudioTrack audioTrack; private int outputPcmFrameSize; private int bufferSize; - private @Nullable AudioTimestampPoller audioTimestampPoller; + @Nullable private AudioTimestampPoller audioTimestampPoller; private int outputSampleRate; private boolean needsPassthroughWorkarounds; private long bufferSizeUs; @@ -144,7 +144,7 @@ import java.lang.reflect.Method; private long smoothedPlayheadOffsetUs; private long lastPlayheadSampleTimeUs; - private @Nullable Method getLatencyMethod; + @Nullable private Method getLatencyMethod; private long latencyUs; private boolean hasData; 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 94f5affb39..e300c65592 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 @@ -88,13 +88,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private static final int MAX_LICENSE_DURATION_TO_RENEW_SECONDS = 60; /** The DRM scheme datas, or null if this session uses offline keys. */ - public final @Nullable List schemeDatas; + @Nullable public final List schemeDatas; private final ExoMediaDrm mediaDrm; private final ProvisioningManager provisioningManager; private final ReleaseCallback releaseCallback; private final @DefaultDrmSessionManager.Mode int mode; - private final @Nullable HashMap optionalKeyRequestParameters; + @Nullable private final HashMap optionalKeyRequestParameters; private final EventDispatcher eventDispatcher; private final int initialDrmRequestRetryCount; @@ -111,8 +111,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private byte @MonotonicNonNull [] sessionId; private byte @MonotonicNonNull [] offlineLicenseKeySetId; - private @Nullable KeyRequest currentKeyRequest; - private @Nullable ProvisionRequest currentProvisionRequest; + @Nullable private KeyRequest currentKeyRequest; + @Nullable private ProvisionRequest currentProvisionRequest; /** * Instantiates a new DRM session. @@ -259,12 +259,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } @Override - public @Nullable Map queryKeyStatus() { + @Nullable + public Map queryKeyStatus() { return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId); } @Override - public @Nullable byte[] getOfflineLicenseKeySetId() { + @Nullable + public byte[] getOfflineLicenseKeySetId() { return offlineLicenseKeySetId; } 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 f27fefa055..7481c60c64 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 @@ -88,7 +88,7 @@ public class DefaultDrmSessionManager private final UUID uuid; private final ExoMediaDrm mediaDrm; private final MediaDrmCallback callback; - private final @Nullable HashMap optionalKeyRequestParameters; + @Nullable private final HashMap optionalKeyRequestParameters; private final EventDispatcher eventDispatcher; private final boolean multiSession; private final int initialDrmRequestRetryCount; @@ -96,9 +96,9 @@ public class DefaultDrmSessionManager private final List> sessions; private final List> provisioningSessions; - private @Nullable Looper playbackLooper; + @Nullable private Looper playbackLooper; private int mode; - private @Nullable byte[] offlineLicenseKeySetId; + @Nullable private byte[] offlineLicenseKeySetId; /* package */ volatile @Nullable MediaDrmHandler mediaDrmHandler; 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 3b05bd1e41..7cc2231e0f 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 @@ -87,7 +87,7 @@ public final class DrmInitData implements Comparator, Parcelable { private int hashCode; /** The protection scheme type, or null if not applicable or unknown. */ - public final @Nullable String schemeType; + @Nullable public final String schemeType; /** * Number of {@link SchemeData}s. @@ -152,7 +152,8 @@ public final class DrmInitData implements Comparator, Parcelable { * @return The initialization data for the scheme, or null if the scheme is not supported. */ @Deprecated - public @Nullable SchemeData get(UUID uuid) { + @Nullable + public SchemeData get(UUID uuid) { for (SchemeData schemeData : schemeDatas) { if (schemeData.matches(uuid)) { return schemeData; @@ -286,11 +287,11 @@ public final class DrmInitData implements Comparator, Parcelable { */ private final UUID uuid; /** The URL of the server to which license requests should be made. May be null if unknown. */ - public final @Nullable String licenseServerUrl; + @Nullable public final String licenseServerUrl; /** The mimeType of {@link #data}. */ public final String mimeType; /** The initialization data. May be null for scheme support checks only. */ - public final @Nullable byte[] data; + @Nullable public final byte[] data; /** * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java index 82fd9a5549..bcc0739042 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java @@ -34,22 +34,26 @@ public final class ErrorStateDrmSession implements Drm } @Override - public @Nullable DrmSessionException getError() { + @Nullable + public DrmSessionException getError() { return error; } @Override - public @Nullable T getMediaCrypto() { + @Nullable + public T getMediaCrypto() { return null; } @Override - public @Nullable Map queryKeyStatus() { + @Nullable + public Map queryKeyStatus() { return null; } @Override - public @Nullable byte[] getOfflineLicenseKeySetId() { + @Nullable + public byte[] getOfflineLicenseKeySetId() { return null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java index caf12948ad..f6b64245fc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java @@ -140,7 +140,7 @@ public final class AmrExtractor implements Extractor { private ExtractorOutput extractorOutput; private TrackOutput trackOutput; - private @Nullable SeekMap seekMap; + @Nullable private SeekMap seekMap; private boolean hasOutputFormat; public AmrExtractor() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java index 116a123094..c0c2080e17 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java @@ -90,7 +90,7 @@ import com.google.android.exoplayer2.util.Util; * Entries are in the range [0, 255], but are stored as long integers for convenience. Null if the * table of contents was missing from the header, in which case seeking is not be supported. */ - private final @Nullable long[] tableOfContents; + @Nullable private final long[] tableOfContents; private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs) { this( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 9bfe383169..572efed1af 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -458,7 +458,8 @@ import java.util.List; * @param type The leaf type. * @return The child leaf of the given type, or null if no such child exists. */ - public @Nullable LeafAtom getLeafAtomOfType(int type) { + @Nullable + public LeafAtom getLeafAtomOfType(int type) { int childrenSize = leafChildren.size(); for (int i = 0; i < childrenSize; i++) { LeafAtom atom = leafChildren.get(i); @@ -478,7 +479,8 @@ import java.util.List; * @param type The container type. * @return The child container of the given type, or null if no such child exists. */ - public @Nullable ContainerAtom getContainerAtomOfType(int type) { + @Nullable + public ContainerAtom getContainerAtomOfType(int type) { int childrenSize = containerChildren.size(); for (int i = 0; i < childrenSize; i++) { ContainerAtom atom = containerChildren.get(i); 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 e0673dd4fa..392d4d9179 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 @@ -122,11 +122,11 @@ public class FragmentedMp4Extractor implements Extractor { // Workarounds. @Flags private final int flags; - private final @Nullable Track sideloadedTrack; + @Nullable private final Track sideloadedTrack; // Sideloaded data. private final List closedCaptionFormats; - private final @Nullable DrmInitData sideloadedDrmInitData; + @Nullable private final DrmInitData sideloadedDrmInitData; // Track-linked data bundle, accessible as a whole through trackID. private final SparseArray trackBundles; @@ -139,13 +139,13 @@ public class FragmentedMp4Extractor implements Extractor { private final ParsableByteArray scratch; // Adjusts sample timestamps. - private final @Nullable TimestampAdjuster timestampAdjuster; + @Nullable private final TimestampAdjuster timestampAdjuster; // Parser state. private final ParsableByteArray atomHeader; private final ArrayDeque containerAtoms; private final ArrayDeque pendingMetadataSampleInfos; - private final @Nullable TrackOutput additionalEmsgTrackOutput; + @Nullable private final TrackOutput additionalEmsgTrackOutput; private int parserState; private int atomType; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index a636d2f680..d1e3217e30 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -85,7 +85,7 @@ public final class AdtsExtractor implements Extractor { private final ParsableBitArray scratchBits; private final long firstStreamSampleTimestampUs; - private @Nullable ExtractorOutput extractorOutput; + @Nullable private ExtractorOutput extractorOutput; private long firstSampleTimestampUs; private long firstFramePosition; 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 730868987a..cd043655ec 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 @@ -94,7 +94,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * to initialize, the {@link DecoderInitializationException} for the fallback decoder. Null if * there was no fallback decoder or no suitable decoders were found. */ - public final @Nullable DecoderInitializationException fallbackDecoderInitializationException; + @Nullable public final DecoderInitializationException fallbackDecoderInitializationException; public DecoderInitializationException(Format format, Throwable cause, boolean secureDecoderRequired, int errorCode) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java index 41cb4ee04a..a639cf9a1b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java @@ -40,7 +40,8 @@ public interface MediaCodecSelector { } @Override - public @Nullable MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { + @Nullable + public MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { return MediaCodecUtil.getPassthroughDecoderInfo(); } }; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index e34b4074fb..a72c70442e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -48,7 +48,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { private final MetadataDecoderFactory decoderFactory; private final MetadataOutput output; - private final @Nullable Handler outputHandler; + @Nullable private final Handler outputHandler; private final FormatHolder formatHolder; private final MetadataInputBuffer buffer; private final Metadata[] pendingMetadata; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java index c233ad61b2..d4bedc63cc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java @@ -31,7 +31,7 @@ public final class ApicFrame extends Id3Frame { public static final String ID = "APIC"; public final String mimeType; - public final @Nullable String description; + @Nullable public final String description; public final int pictureType; public final byte[] pictureData; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 4417126427..85a59c3aeb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -82,7 +82,7 @@ public final class Id3Decoder implements MetadataDecoder { private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; private static final int ID3_TEXT_ENCODING_UTF_8 = 3; - private final @Nullable FramePredicate framePredicate; + @Nullable private final FramePredicate framePredicate; public Id3Decoder() { this(null); @@ -97,7 +97,8 @@ public final class Id3Decoder implements MetadataDecoder { @SuppressWarnings("ByteBufferBackingArray") @Override - public @Nullable Metadata decode(MetadataInputBuffer inputBuffer) { + @Nullable + public Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = inputBuffer.data; return decode(buffer.array(), buffer.limit()); } @@ -110,7 +111,8 @@ public final class Id3Decoder implements MetadataDecoder { * @return A {@link Metadata} object containing the decoded ID3 tags, or null if the data could * not be decoded. */ - public @Nullable Metadata decode(byte[] data, int size) { + @Nullable + public Metadata decode(byte[] data, int size) { List id3Frames = new ArrayList<>(); ParsableByteArray id3Data = new ParsableByteArray(data, size); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java index 8a36276b91..0e129ca7bb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.Util; */ public final class TextInformationFrame extends Id3Frame { - public final @Nullable String description; + @Nullable public final String description; public final String value; public TextInformationFrame(String id, @Nullable String description, String value) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java index 8be9ed1881..298558b662 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.Util; */ public final class UrlLinkFrame extends Id3Frame { - public final @Nullable String description; + @Nullable public final String description; public final String url; public UrlLinkFrame(String id, @Nullable String description, String url) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java index 9323f7505c..06db088f06 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java @@ -34,8 +34,8 @@ public abstract class CompositeMediaSource extends BaseMediaSource { private final HashMap childSources; - private @Nullable Handler eventHandler; - private @Nullable TransferListener mediaTransferListener; + @Nullable private Handler eventHandler; + @Nullable private TransferListener mediaTransferListener; /** Create composite media source without child sources. */ protected CompositeMediaSource() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 841f18bab4..d9003e443e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -64,9 +64,9 @@ public final class ExtractorMediaSource extends BaseMediaSource private final DataSource.Factory dataSourceFactory; - private @Nullable ExtractorsFactory extractorsFactory; - private @Nullable String customCacheKey; - private @Nullable Object tag; + @Nullable private ExtractorsFactory extractorsFactory; + @Nullable private String customCacheKey; + @Nullable private Object tag; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private int continueLoadingCheckIntervalBytes; private boolean isCreateCalled; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java index 233e19b29c..ab8d86cc55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -101,7 +101,7 @@ public interface MediaSourceEventListener { * The format of the track to which the data belongs. Null if the data does not belong to a * specific track. */ - public final @Nullable Format trackFormat; + @Nullable public final Format trackFormat; /** * One of the {@link C} {@code SELECTION_REASON_*} constants if the data belongs to a track. * {@link C#SELECTION_REASON_UNKNOWN} otherwise. @@ -111,7 +111,7 @@ public interface MediaSourceEventListener { * Optional data associated with the selection of the track to which the data belongs. Null if * the data does not belong to a track. */ - public final @Nullable Object trackSelectionData; + @Nullable public final Object trackSelectionData; /** * The start time of the media, or {@link C#TIME_UNSET} if the data does not belong to a * specific media period. @@ -296,7 +296,7 @@ public interface MediaSourceEventListener { /** The timeline window index reported with the events. */ public final int windowIndex; /** The {@link MediaPeriodId} reported with the events. */ - public final @Nullable MediaPeriodId mediaPeriodId; + @Nullable public final MediaPeriodId mediaPeriodId; private final CopyOnWriteArrayList listenerAndHandlers; private final long mediaTimeOffsetMs; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index d9f0008a7f..e8f630f202 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -1013,7 +1013,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private final Extractor[] extractors; - private @Nullable Extractor extractor; + @Nullable private Extractor extractor; /** * Creates a holder that will select an extractor and initialize it using the specified output. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index acdfbcc8c0..14648775f8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -35,7 +35,7 @@ public final class SinglePeriodTimeline extends Timeline { private final long windowDefaultStartPositionUs; private final boolean isSeekable; private final boolean isDynamic; - private final @Nullable Object tag; + @Nullable private final Object tag; /** * Creates a timeline containing a single period and a window that spans it. 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 6063168e99..62d873868e 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 @@ -53,7 +53,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final DataSpec dataSpec; private final DataSource.Factory dataSourceFactory; - private final @Nullable TransferListener transferListener; + @Nullable private final TransferListener transferListener; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final TrackGroupArray tracks; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 6f85a2b0f8..55d967cd69 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -60,7 +60,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private boolean treatLoadErrorsAsEndOfStream; private boolean isCreateCalled; - private @Nullable Object tag; + @Nullable private Object tag; /** * Creates a factory for {@link SingleSampleMediaSource}s. @@ -186,7 +186,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { private final Timeline timeline; @Nullable private final Object tag; - private @Nullable TransferListener transferListener; + @Nullable private TransferListener transferListener; /** * @param uri The {@link Uri} of the media stream. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java index 2e7581eba5..a794f67fe2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java @@ -56,7 +56,7 @@ public abstract class Chunk implements Loadable { * Optional data associated with the selection of the track to which this chunk belongs. Null if * the chunk does not belong to a track. */ - public final @Nullable Object trackSelectionData; + @Nullable public final Object trackSelectionData; /** * The start time of the media contained by the chunk, or {@link C#TIME_UNSET} if the data * being loaded does not contain media samples. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 18eada4708..d7a19fa9d4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -76,7 +76,7 @@ public class ChunkSampleStream implements SampleStream, S private final BaseMediaChunkOutput mediaChunkOutput; private Format primaryDownstreamTrackFormat; - private @Nullable ReleaseCallback releaseCallback; + @Nullable private ReleaseCallback releaseCallback; private long pendingResetPositionUs; private long lastSeekPositionUs; private int nextNotifyPrimaryFormatMediaChunkIndex; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 55bee5bd6a..bdf127be59 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -77,7 +77,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { private static final int MSG_UPDATE_OUTPUT = 0; - private final @Nullable Handler outputHandler; + @Nullable private final Handler outputHandler; private final TextOutput output; private final SubtitleDecoderFactory decoderFactory; private final FormatHolder formatHolder; 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 bbf57c5602..08f4d3a928 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 @@ -39,7 +39,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { /** Factory for {@link AdaptiveTrackSelection} instances. */ public static class Factory implements TrackSelection.Factory { - private final @Nullable BandwidthMeter bandwidthMeter; + @Nullable private final BandwidthMeter bandwidthMeter; private final int minDurationForQualityIncreaseMs; private final int maxDurationForQualityDecreaseMs; private final int minDurationToRetainAfterDiscardMs; @@ -537,7 +537,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { } @Override - public @Nullable Object getSelectionData() { + @Nullable + public Object getSelectionData() { return null; } 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 3bdaeeeafb..fefad00cbd 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 @@ -39,7 +39,7 @@ public final class FixedTrackSelection extends BaseTrackSelection { public static final class Factory implements TrackSelection.Factory { private final int reason; - private final @Nullable Object data; + @Nullable private final Object data; public Factory() { this.reason = C.SELECTION_REASON_UNKNOWN; @@ -66,7 +66,7 @@ public final class FixedTrackSelection extends BaseTrackSelection { } private final int reason; - private final @Nullable Object data; + @Nullable private final Object data; /** * @param group The {@link TrackGroup}. Must not be null. @@ -109,7 +109,8 @@ public final class FixedTrackSelection extends BaseTrackSelection { } @Override - public @Nullable Object getSelectionData() { + @Nullable + public 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 dfb19e3bca..5587af9cbf 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 @@ -312,7 +312,7 @@ public abstract class MappingTrackSelector extends TrackSelector { } - private @Nullable MappedTrackInfo currentMappedTrackInfo; + @Nullable private MappedTrackInfo currentMappedTrackInfo; /** * Returns the mapping information for the currently active track selection, or null if no 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 8053212969..f35e7ec755 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 @@ -135,7 +135,8 @@ public final class RandomTrackSelection extends BaseTrackSelection { } @Override - public @Nullable Object getSelectionData() { + @Nullable + public Object getSelectionData() { return null; } 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 bc905ace4b..fc20e863ba 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 @@ -42,7 +42,8 @@ public final class TrackSelectionArray { * @param index The index of the selection. * @return The selection. */ - public @Nullable TrackSelection get(int index) { + @Nullable + public TrackSelection get(int index) { return trackSelections[index]; } 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 f2fbd89118..fb74bd9d54 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 @@ -98,8 +98,8 @@ public abstract class TrackSelector { } - private @Nullable InvalidationListener listener; - private @Nullable BandwidthMeter bandwidthMeter; + @Nullable private InvalidationListener listener; + @Nullable private BandwidthMeter bandwidthMeter; /** * Called by the player to initialize the selector. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java index 21f2d5993a..80687db31f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java @@ -33,7 +33,7 @@ public abstract class BaseDataSource implements DataSource { private final ArrayList listeners; private int listenerCount; - private @Nullable DataSpec dataSpec; + @Nullable private DataSpec dataSpec; /** * Creates base data source. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index a98f773c9d..99a3d271bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -97,10 +97,10 @@ public final class DataSpec { /** * The HTTP body, null otherwise. If the body is non-null, then httpBody.length will be non-zero. */ - public final @Nullable byte[] httpBody; + @Nullable public final byte[] httpBody; /** @deprecated Use {@link #httpBody} instead. */ - @Deprecated public final @Nullable byte[] postBody; + @Deprecated @Nullable public final byte[] postBody; /** * The absolute position of the data in the full stream. @@ -121,7 +121,7 @@ public final class DataSpec { * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the * data spec is not intended to be used in conjunction with a cache. */ - public final @Nullable String key; + @Nullable public final String key; /** Request {@link Flags flags}. */ public final @Flags int flags; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java index aa13baa03e..bfc9a37844 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -62,14 +62,14 @@ public final class DefaultDataSource implements DataSource { private final DataSource baseDataSource; // Lazily initialized. - private @Nullable DataSource fileDataSource; - private @Nullable DataSource assetDataSource; - private @Nullable DataSource contentDataSource; - private @Nullable DataSource rtmpDataSource; - private @Nullable DataSource dataSchemeDataSource; - private @Nullable DataSource rawResourceDataSource; + @Nullable private DataSource fileDataSource; + @Nullable private DataSource assetDataSource; + @Nullable private DataSource contentDataSource; + @Nullable private DataSource rtmpDataSource; + @Nullable private DataSource dataSchemeDataSource; + @Nullable private DataSource rawResourceDataSource; - private @Nullable DataSource dataSource; + @Nullable private DataSource dataSource; /** * Constructs a new instance, optionally configured to follow cross-protocol redirects. @@ -178,7 +178,8 @@ public final class DefaultDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return dataSource == null ? null : dataSource.getUri(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java index a5dfad72f0..6b1131a3bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.upstream.DataSource.Factory; public final class DefaultDataSourceFactory implements Factory { private final Context context; - private final @Nullable TransferListener listener; + @Nullable private final TransferListener listener; private final DataSource.Factory baseDataSourceFactory; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index 5955a5d9d9..0d16a3f20e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -228,7 +228,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return connection == null ? null : Uri.parse(connection.getURL().toString()); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java index e0b1efad54..f5d7dbd24c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java @@ -24,7 +24,7 @@ import com.google.android.exoplayer2.util.Assertions; public final class DefaultHttpDataSourceFactory extends BaseFactory { private final String userAgent; - private final @Nullable TransferListener listener; + @Nullable private final TransferListener listener; private final int connectTimeoutMillis; private final int readTimeoutMillis; private final boolean allowCrossProtocolRedirects; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java index 3a47df7654..0b4de1b43e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java @@ -22,7 +22,7 @@ import androidx.annotation.Nullable; */ public final class FileDataSourceFactory implements DataSource.Factory { - private final @Nullable TransferListener listener; + @Nullable private final TransferListener listener; public FileDataSourceFactory() { this(null); 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 878c40dc9e..b5a13f3b80 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 @@ -311,7 +311,7 @@ public final class Loader implements LoaderErrorThrower { private final T loadable; private final long startTimeMs; - private @Nullable Loader.Callback callback; + @Nullable private Loader.Callback callback; private IOException currentError; private int errorCount; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java index 62e68cd920..767b6d78a3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java @@ -71,7 +71,8 @@ public final class PriorityDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return upstream.getUri(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/StatsDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/StatsDataSource.java index b7a01505f8..6cdc381ba2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/StatsDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/StatsDataSource.java @@ -96,7 +96,8 @@ public final class StatsDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return dataSource.getUri(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java index ecf25f2eb6..f56f19a6ca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java @@ -80,7 +80,8 @@ public final class TeeDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return upstream.getUri(); } 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 58b2d176cf..058489f8f0 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 @@ -123,7 +123,7 @@ public final class CacheDataSource implements DataSource { private final Cache cache; private final DataSource cacheReadDataSource; - private final @Nullable DataSource cacheWriteDataSource; + @Nullable private final DataSource cacheWriteDataSource; private final DataSource upstreamDataSource; private final CacheKeyFactory cacheKeyFactory; @Nullable private final EventListener eventListener; @@ -132,16 +132,16 @@ public final class CacheDataSource implements DataSource { private final boolean ignoreCacheOnError; private final boolean ignoreCacheForUnsetLengthRequests; - private @Nullable DataSource currentDataSource; + @Nullable private DataSource currentDataSource; private boolean currentDataSpecLengthUnset; - private @Nullable Uri uri; - private @Nullable Uri actualUri; + @Nullable private Uri uri; + @Nullable private Uri actualUri; private @HttpMethod int httpMethod; private int flags; - private @Nullable String key; + @Nullable private String key; private long readPosition; private long bytesRemaining; - private @Nullable CacheSpan currentHoleSpan; + @Nullable private CacheSpan currentHoleSpan; private boolean seenCacheError; private boolean currentRequestIgnoresCache; private long totalCachedBytesRead; @@ -329,7 +329,8 @@ public final class CacheDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return actualUri; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java index 1e8cf1517d..609e933c9d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java @@ -41,10 +41,8 @@ public class CacheSpan implements Comparable { * Whether the {@link CacheSpan} is cached. */ public final boolean isCached; - /** - * The file corresponding to this {@link CacheSpan}, or null if {@link #isCached} is false. - */ - public final @Nullable File file; + /** The file corresponding to this {@link CacheSpan}, or null if {@link #isCached} is false. */ + @Nullable public final File file; /** The last touch timestamp, or {@link C#TIME_UNSET} if {@link #isCached} is false. */ public final long lastTouchTimestamp; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java index 644338c8eb..0910c63c19 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java @@ -71,7 +71,8 @@ public final class AesCipherDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return upstream.getUri(); } 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 33b50934f1..e72e72c3c4 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 @@ -83,12 +83,12 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL private final Handler handler; private final int[] textureIdHolder; - private final @Nullable TextureImageListener callback; + @Nullable private final TextureImageListener callback; - private @Nullable EGLDisplay display; - private @Nullable EGLContext context; - private @Nullable EGLSurface surface; - private @Nullable SurfaceTexture texture; + @Nullable private EGLDisplay display; + @Nullable private EGLContext context; + @Nullable private EGLSurface surface; + @Nullable private SurfaceTexture texture; /** * @param handler The {@link Handler} that will be used to call {@link diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index 7a2ea5daf2..cde9a351d9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -54,7 +54,7 @@ public class EventLogger implements AnalyticsListener { TIME_FORMAT.setGroupingUsed(false); } - private final @Nullable MappingTrackSelector trackSelector; + @Nullable private final MappingTrackSelector trackSelector; private final String tag; private final Timeline.Window window; private final Timeline.Period period; 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 0c5116624e..67686ad64f 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 @@ -490,7 +490,8 @@ public final class ParsableByteArray { * @return The string not including any terminating NUL byte, or null if the end of the data has * already been reached. */ - public @Nullable String readNullTerminatedString() { + @Nullable + public String readNullTerminatedString() { if (bytesLeft() == 0) { return null; } @@ -516,7 +517,8 @@ public final class ParsableByteArray { * @return The line not including any line-termination characters, or null if the end of the data * has already been reached. */ - public @Nullable String readLine() { + @Nullable + public String readLine() { if (bytesLeft() == 0) { return null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/TimedValueQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/util/TimedValueQueue.java index 3ac76eb54c..da5d9bafeb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/TimedValueQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/TimedValueQueue.java @@ -97,7 +97,8 @@ public final class TimedValueQueue { * @return The value with the closest timestamp or null if the buffer is empty or there is no * older value and {@code onlyOlder} is true. */ - private @Nullable V poll(long timestamp, boolean onlyOlder) { + @Nullable + private V poll(long timestamp, boolean onlyOlder) { V value = null; long previousTimeDiff = Long.MAX_VALUE; while (size > 0) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java index 1b3943caf7..ed2ca9c034 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java @@ -51,7 +51,7 @@ public final class ColorInfo implements Parcelable { public final int colorTransfer; /** HdrStaticInfo as defined in CTA-861.3, or null if none specified. */ - public final @Nullable byte[] hdrStaticInfo; + @Nullable public final byte[] hdrStaticInfo; // Lazily initialized hashcode. private int hashCode; 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 f302279f06..920d569fd3 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 @@ -158,9 +158,9 @@ public final class DummySurface extends Surface { private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexture; private @MonotonicNonNull Handler handler; - private @Nullable Error initError; - private @Nullable RuntimeException initException; - private @Nullable DummySurface surface; + @Nullable private Error initError; + @Nullable private RuntimeException initException; + @Nullable private DummySurface surface; public DummySurfaceThread() { super("dummySurface"); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java b/library/core/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java index 727883f678..bb11ef0005 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java @@ -27,7 +27,7 @@ import java.util.List; */ public final class HevcConfig { - public final @Nullable List initializationData; + @Nullable public final List initializationData; public final int nalUnitLengthFieldLength; /** 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 fe9996bfc2..7193c4c22b 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 @@ -138,7 +138,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private long lastInputTimeUs; private long outputStreamOffsetUs; private int pendingOutputStreamOffsetCount; - private @Nullable VideoFrameMetadataListener frameMetadataListener; + @Nullable private VideoFrameMetadataListener frameMetadataListener; /** * @param context A context. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java index eb7110834b..03822be17c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java @@ -39,7 +39,7 @@ public class CameraMotionRenderer extends BaseRenderer { private final ParsableByteArray scratch; private long offsetUs; - private @Nullable CameraMotionListener listener; + @Nullable private CameraMotionListener listener; private long lastTimestampUs; public CameraMotionRenderer() { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index 2e9b539096..22aa63b83a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -847,7 +847,7 @@ public final class AnalyticsCollectorTest { private static final class EventWindowAndPeriodId { private final int windowIndex; - private final @Nullable MediaPeriodId mediaPeriodId; + @Nullable private final MediaPeriodId mediaPeriodId; public EventWindowAndPeriodId(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { this.windowIndex = windowIndex; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java index 2426073d8a..1eb49188bf 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java @@ -107,7 +107,8 @@ public class BaseDataSourceTest { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { throw new UnsupportedOperationException(); } 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 aa080bbdec..be0aa4f154 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 @@ -69,7 +69,7 @@ import java.util.regex.Pattern; /* package */ final int id; private final DashChunkSource.Factory chunkSourceFactory; - private final @Nullable TransferListener transferListener; + @Nullable private final TransferListener transferListener; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long elapsedRealtimeOffsetMs; private final LoaderErrorThrower manifestLoaderErrorThrower; @@ -82,7 +82,7 @@ import java.util.regex.Pattern; trackEmsgHandlerBySampleStream; private final EventDispatcher eventDispatcher; - private @Nullable Callback callback; + @Nullable private Callback callback; private ChunkSampleStream[] sampleStreams; private EventSampleStream[] eventSampleStreams; private SequenceableLoader compositeSequenceableLoader; 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 709fd00ea7..779a97fd09 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 @@ -373,11 +373,11 @@ public final class DashMediaSource extends BaseMediaSource { private final Runnable simulateManifestRefreshRunnable; private final PlayerEmsgCallback playerEmsgCallback; private final LoaderErrorThrower manifestLoadErrorThrower; - private final @Nullable Object tag; + @Nullable private final Object tag; private DataSource dataSource; private Loader loader; - private @Nullable TransferListener mediaTransferListener; + @Nullable private TransferListener mediaTransferListener; private IOException manifestFatalError; private Handler handler; @@ -1139,7 +1139,7 @@ public final class DashMediaSource extends BaseMediaSource { private final long windowDurationUs; private final long windowDefaultStartPositionUs; private final DashManifest manifest; - private final @Nullable Object windowTag; + @Nullable private final Object windowTag; public DashTimeline( long presentationStartTimeMs, 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 057f0262d0..2de81a2535 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 @@ -617,7 +617,7 @@ public class DefaultDashChunkSource implements DashChunkSource { /* package */ final @Nullable ChunkExtractorWrapper extractorWrapper; public final Representation representation; - public final @Nullable DashSegmentIndex segmentIndex; + @Nullable public final DashSegmentIndex segmentIndex; private final long periodDurationUs; private final long segmentNumShift; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java index c7bb4adec5..9ac1257ee2 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java @@ -86,7 +86,8 @@ public final class RangedUri { * @param baseUri The optional base Uri. * @return The merged {@link RangedUri} if the merge was successful. Null otherwise. */ - public @Nullable RangedUri attemptMerge(@Nullable RangedUri other, String baseUri) { + @Nullable + public RangedUri attemptMerge(@Nullable RangedUri other, String baseUri) { final String resolvedUri = resolveUriString(baseUri); if (other == null || !resolvedUri.equals(other.resolveUriString(baseUri))) { return null; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java index 4fe76cdf81..022d62cbfc 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java @@ -51,7 +51,7 @@ import javax.crypto.spec.SecretKeySpec; private final byte[] encryptionKey; private final byte[] encryptionIv; - private @Nullable CipherInputStream cipherInputStream; + @Nullable private CipherInputStream cipherInputStream; /** * @param upstream The upstream {@link DataSource}. 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 2cfd14c79d..d834c097cf 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 @@ -62,7 +62,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final HlsExtractorFactory extractorFactory; private final HlsPlaylistTracker playlistTracker; private final HlsDataSourceFactory dataSourceFactory; - private final @Nullable TransferListener mediaTransferListener; + @Nullable private final TransferListener mediaTransferListener; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Allocator allocator; @@ -72,7 +72,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final boolean allowChunklessPreparation; private final boolean useSessionKeys; - private @Nullable Callback callback; + @Nullable private Callback callback; private int pendingPrepareCount; private TrackGroupArray trackGroups; private HlsSampleStreamWrapper[] sampleStreamWrappers; 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 be4484aa78..fbb6285d1d 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 @@ -301,9 +301,9 @@ public final class HlsMediaSource extends BaseMediaSource private final boolean allowChunklessPreparation; private final boolean useSessionKeys; private final HlsPlaylistTracker playlistTracker; - private final @Nullable Object tag; + @Nullable private final Object tag; - private @Nullable TransferListener mediaTransferListener; + @Nullable private TransferListener mediaTransferListener; private HlsMediaSource( Uri manifestUri, 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 0064338ca8..a4fd28009f 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 @@ -166,7 +166,8 @@ public final class DefaultHlsPlaylistTracker } @Override - public @Nullable HlsMasterPlaylist getMasterPlaylist() { + @Nullable + public HlsMasterPlaylist getMasterPlaylist() { return masterPlaylist; } 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 135ee4a58e..38781782eb 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 @@ -42,7 +42,7 @@ import java.util.List; implements MediaPeriod, SequenceableLoader.Callback> { private final SsChunkSource.Factory chunkSourceFactory; - private final @Nullable TransferListener transferListener; + @Nullable private final TransferListener transferListener; private final LoaderErrorThrower manifestLoaderErrorThrower; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; @@ -50,7 +50,7 @@ import java.util.List; private final TrackGroupArray trackGroups; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private @Nullable Callback callback; + @Nullable private Callback callback; private SsManifest manifest; private ChunkSampleStream[] sampleStreams; private SequenceableLoader compositeSequenceableLoader; diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 7b9f3e3c4f..3c18bfe644 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -323,12 +323,12 @@ public final class SsMediaSource extends BaseMediaSource private final EventDispatcher manifestEventDispatcher; private final ParsingLoadable.Parser manifestParser; private final ArrayList mediaPeriods; - private final @Nullable Object tag; + @Nullable private final Object tag; private DataSource manifestDataSource; private Loader manifestLoader; private LoaderErrorThrower manifestLoaderErrorThrower; - private @Nullable TransferListener mediaTransferListener; + @Nullable private TransferListener mediaTransferListener; private long manifestLoadStartTimestamp; private SsManifest manifest; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java index 5c70203788..69a2cf96be 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java @@ -190,7 +190,7 @@ public class DefaultTimeBar extends View implements TimeBar { private final Paint adMarkerPaint; private final Paint playedAdMarkerPaint; private final Paint scrubberPaint; - private final @Nullable Drawable scrubberDrawable; + @Nullable private final Drawable scrubberDrawable; private final int barHeight; private final int touchTargetHeight; private final int adMarkerWidth; @@ -217,8 +217,8 @@ public class DefaultTimeBar extends View implements TimeBar { private long position; private long bufferedPosition; private int adGroupCount; - private @Nullable long[] adGroupTimesMs; - private @Nullable boolean[] playedAdGroups; + @Nullable private long[] adGroupTimesMs; + @Nullable private boolean[] playedAdGroups; public DefaultTimeBar(Context context) { this(context, null); 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 5bb8324780..3575969a78 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 @@ -621,7 +621,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } /** Returns the default artwork to display. */ - public @Nullable Drawable getDefaultArtwork() { + @Nullable + public Drawable getDefaultArtwork() { return defaultArtwork; } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java index f24bcce3ce..8a211d0879 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java @@ -91,8 +91,8 @@ import org.checkerframework.checker.nullness.qual.Nullable; }; private int stereoMode; - private @Nullable MeshData leftMeshData; - private @Nullable MeshData rightMeshData; + @Nullable private MeshData leftMeshData; + @Nullable private MeshData rightMeshData; // Program related GL items. These are only valid if program != 0. private int program; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java index 2889351f19..b70fd277a9 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java @@ -54,7 +54,7 @@ public final class SceneRenderer implements VideoFrameMetadataListener, CameraMo // Used by other threads only private volatile @C.StreamType int defaultStereoMode; private @C.StreamType int lastStereoMode; - private @Nullable byte[] lastProjectionData; + @Nullable private byte[] lastProjectionData; // Methods called on any thread. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java index 8f82ae17db..67bc992558 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java @@ -64,14 +64,14 @@ public final class SphericalSurfaceView extends GLSurfaceView { /* package */ static final float UPRIGHT_ROLL = (float) Math.PI; private final SensorManager sensorManager; - private final @Nullable Sensor orientationSensor; + @Nullable private final Sensor orientationSensor; private final OrientationListener orientationListener; private final Handler mainHandler; private final TouchTracker touchTracker; private final SceneRenderer scene; - private @Nullable SurfaceTexture surfaceTexture; - private @Nullable Surface surface; - private @Nullable Player.VideoComponent videoComponent; + @Nullable private SurfaceTexture surfaceTexture; + @Nullable private Surface surface; + @Nullable private Player.VideoComponent videoComponent; public SphericalSurfaceView(Context context) { this(context, null); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/TouchTracker.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/TouchTracker.java index 142f2fc668..5f3a5275c1 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/TouchTracker.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/TouchTracker.java @@ -65,7 +65,7 @@ import android.view.View; // The conversion from touch to yaw & pitch requires compensating for device roll. This is set // on the sensor thread and read on the UI thread. private volatile float roll; - private @Nullable SingleTapListener singleTapListener; + @Nullable private SingleTapListener singleTapListener; @SuppressWarnings({ "nullness:assignment.type.incompatible", 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 facbe8bbde..93e52bc23a 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 @@ -43,7 +43,7 @@ import com.google.android.exoplayer2.util.Log; public abstract class Action { private final String tag; - private final @Nullable String description; + @Nullable private final String description; /** * @param tag A tag to use for logging. @@ -547,7 +547,7 @@ public abstract class Action { */ public static final class WaitForTimelineChanged extends Action { - private final @Nullable Timeline expectedTimeline; + @Nullable private final Timeline expectedTimeline; /** * Creates action waiting for a timeline change. diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 7f688cacf7..735156e64c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -613,7 +613,7 @@ public final class ActionSchedule { */ private static final class CallbackAction extends Action { - private @Nullable Callback callback; + @Nullable private Callback callback; public CallbackAction(String tag) { super(tag, "FinishedCallback"); 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 0d55dd8530..cc4b3a60d7 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 @@ -338,9 +338,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private final DefaultTrackSelector trackSelector; private final LoadControl loadControl; private final BandwidthMeter bandwidthMeter; - private final @Nullable ActionSchedule actionSchedule; - private final @Nullable Player.EventListener eventListener; - private final @Nullable AnalyticsListener analyticsListener; + @Nullable private final ActionSchedule actionSchedule; + @Nullable private final Player.EventListener eventListener; + @Nullable private final AnalyticsListener analyticsListener; private final HandlerThread playerThread; private final HandlerWrapper handler; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index 1e3b3bf82b..fea863c48e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -40,7 +40,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod private final Allocator allocator; private final FakeChunkSource.Factory chunkSourceFactory; - private final @Nullable TransferListener transferListener; + @Nullable private final TransferListener transferListener; private final long durationUs; private Callback callback; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java index 77ae19f083..286ef15b15 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java @@ -79,11 +79,11 @@ public class FakeDataSet { */ public static final class Segment { - public @Nullable final IOException exception; - public @Nullable final byte[] data; + @Nullable public final IOException exception; + @Nullable public final byte[] data; public final int length; public final long byteOffset; - public @Nullable final Runnable action; + @Nullable public final Runnable action; public boolean exceptionThrown; public boolean exceptionCleared; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index b89acae6c8..0d50f22bc0 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -60,7 +60,7 @@ public class FakeMediaSource extends BaseMediaSource { private boolean preparedSource; private boolean releasedSource; private Handler sourceInfoRefreshHandler; - private @Nullable TransferListener transferListener; + @Nullable private TransferListener transferListener; /** * Creates a {@link FakeMediaSource}. This media source creates {@link FakeMediaPeriod}s with a diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java index a60c1c9c6d..02d0e372e8 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java @@ -31,7 +31,7 @@ import java.io.IOException; public final class FakeSampleStream implements SampleStream { private final Format format; - private final @Nullable EventDispatcher eventDispatcher; + @Nullable private final EventDispatcher eventDispatcher; private boolean notifiedDownstreamFormat; private boolean readFormat; From 2c4eb1cd9bff6dd39a51a946872de79635ba19ea Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 28 May 2019 11:30:23 +0100 Subject: [PATCH 1314/1556] Demo app change. PiperOrigin-RevId: 250248268 --- .../com/google/android/exoplayer2/demo/PlayerActivity.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 f7db8c7ca3..82fb8bb9f5 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 @@ -151,7 +151,8 @@ public class PlayerActivity extends AppCompatActivity @Override public void onCreate(Bundle savedInstanceState) { - String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA); + Intent intent = getIntent(); + String sphericalStereoMode = intent.getStringExtra(SPHERICAL_STEREO_MODE_EXTRA); if (sphericalStereoMode != null) { setTheme(R.style.PlayerTheme_Spherical); } From 6b68bc0c9d977e3cace2b5c13184563d971ba249 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 28 May 2019 12:20:14 +0100 Subject: [PATCH 1315/1556] Fix anchor usage in SubtitlePainter's setupBitmapLayout According to Cue's constructor (for bitmaps) documentation: + cuePositionAnchor does horizontal anchoring. + cueLineAnchor does vertical anchoring. Usage is currently inverted. Issue:#5633 PiperOrigin-RevId: 250253002 --- .../android/exoplayer2/ui/SubtitlePainter.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 4f22362de6..9ed1bbd006 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 @@ -362,10 +362,16 @@ import com.google.android.exoplayer2.util.Util; int width = Math.round(parentWidth * cueSize); int height = cueBitmapHeight != Cue.DIMEN_UNSET ? Math.round(parentHeight * cueBitmapHeight) : Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); - int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) - : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX); - int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) - : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY); + int x = + Math.round( + cuePositionAnchor == Cue.ANCHOR_TYPE_END + ? (anchorX - width) + : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX); + int y = + Math.round( + cueLineAnchor == Cue.ANCHOR_TYPE_END + ? (anchorY - height) + : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY); bitmapRect = new Rect(x, y, x + width, y + height); } From 47cc567dcaf26bf9ee673e0346317dc15a80ac1e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 28 May 2019 15:33:57 +0100 Subject: [PATCH 1316/1556] Add reference counting to DrmSession This CL should not introduce any functional changes. PiperOrigin-RevId: 250277165 --- .../ext/vp9/LibvpxVideoRenderer.java | 20 ++--- .../android/exoplayer2/FormatHolder.java | 6 +- .../audio/SimpleDecoderAudioRenderer.java | 20 ++--- .../exoplayer2/drm/DefaultDrmSession.java | 82 ++++++++++--------- .../drm/DefaultDrmSessionManager.java | 11 +-- .../android/exoplayer2/drm/DrmSession.java | 28 +++++++ .../exoplayer2/drm/DrmSessionManager.java | 12 +-- .../exoplayer2/drm/ErrorStateDrmSession.java | 9 ++ .../exoplayer2/drm/OfflineLicenseHelper.java | 4 +- .../mediacodec/MediaCodecRenderer.java | 20 ++--- 10 files changed, 105 insertions(+), 107 deletions(-) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index d5da9a011d..5871371d76 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -473,21 +473,13 @@ public class LibvpxVideoRenderer extends BaseRenderer { } private void setSourceDrmSession(@Nullable DrmSession session) { - DrmSession previous = sourceDrmSession; + DrmSession.replaceSessionReferences(sourceDrmSession, session); sourceDrmSession = session; - releaseDrmSessionIfUnused(previous); } private void setDecoderDrmSession(@Nullable DrmSession session) { - DrmSession previous = decoderDrmSession; + DrmSession.replaceSessionReferences(decoderDrmSession, session); decoderDrmSession = session; - releaseDrmSessionIfUnused(previous); - } - - private void releaseDrmSessionIfUnused(@Nullable DrmSession session) { - if (session != null && session != decoderDrmSession && session != sourceDrmSession) { - drmSessionManager.releaseSession(session); - } } /** @@ -512,12 +504,10 @@ public class LibvpxVideoRenderer extends BaseRenderer { } DrmSession session = drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); - if (session == decoderDrmSession || session == sourceDrmSession) { - // We already had this session. The manager must be reference counting, so release it once - // to get the count attributed to this renderer back down to 1. - drmSessionManager.releaseSession(session); + if (sourceDrmSession != null) { + sourceDrmSession.releaseReference(); } - setSourceDrmSession(session); + sourceDrmSession = session; } else { setSourceDrmSession(null); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java index 5da8d0f9f5..bd73eaf4fb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer2; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.drm.DecryptionResource; +import com.google.android.exoplayer2.drm.DrmSession; /** * Holds a {@link Format}. @@ -25,14 +25,14 @@ public final class FormatHolder { /** * Whether the object expected to populate {@link #format} is also expected to populate {@link - * #decryptionResource}. + * #drmSession}. */ // TODO: Remove once all Renderers and MediaSources have migrated to the new DRM model [Internal // ref: b/129764794]. public boolean decryptionResourceIsProvided; /** An accompanying context for decrypting samples in the format. */ - @Nullable public DecryptionResource decryptionResource; + @Nullable public DrmSession drmSession; /** The held {@link Format}. */ @Nullable public Format format; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 553dfb1187..1553227988 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -646,21 +646,13 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } private void setSourceDrmSession(@Nullable DrmSession session) { - DrmSession previous = sourceDrmSession; + DrmSession.replaceSessionReferences(sourceDrmSession, session); sourceDrmSession = session; - releaseDrmSessionIfUnused(previous); } private void setDecoderDrmSession(@Nullable DrmSession session) { - DrmSession previous = decoderDrmSession; + DrmSession.replaceSessionReferences(decoderDrmSession, session); decoderDrmSession = session; - releaseDrmSessionIfUnused(previous); - } - - private void releaseDrmSessionIfUnused(@Nullable DrmSession session) { - if (session != null && session != decoderDrmSession && session != sourceDrmSession) { - drmSessionManager.releaseSession(session); - } } private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { @@ -677,12 +669,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } DrmSession session = drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); - if (session == decoderDrmSession || session == sourceDrmSession) { - // We already had this session. The manager must be reference counting, so release it once - // to get the count attributed to this renderer back down to 1. - drmSessionManager.releaseSession(session); + if (sourceDrmSession != null) { + sourceDrmSession.releaseReference(); } - setSourceDrmSession(session); + sourceDrmSession = session; } else { setSourceDrmSession(null); } 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 e300c65592..e49602957f 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 @@ -38,6 +38,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -103,12 +104,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /* package */ final PostResponseHandler postResponseHandler; private @DrmSession.State int state; - private int openCount; + private int referenceCount; @Nullable private HandlerThread requestHandlerThread; @Nullable private PostRequestHandler postRequestHandler; @Nullable private T mediaCrypto; @Nullable private DrmSessionException lastException; - private byte @MonotonicNonNull [] sessionId; + private byte @NullableType [] sessionId; private byte @MonotonicNonNull [] offlineLicenseKeySetId; @Nullable private KeyRequest currentKeyRequest; @@ -169,42 +170,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; postResponseHandler = new PostResponseHandler(playbackLooper); } - // Life cycle. - - public void acquire() { - if (++openCount == 1) { - requestHandlerThread = new HandlerThread("DrmRequestHandler"); - requestHandlerThread.start(); - postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); - if (openInternal(true)) { - doLicense(true); - } - } - } - - @SuppressWarnings("assignment.type.incompatible") - public void release() { - if (--openCount == 0) { - // Assigning null to various non-null variables for clean-up. - state = STATE_RELEASED; - postResponseHandler.removeCallbacksAndMessages(null); - Util.castNonNull(postRequestHandler).removeCallbacksAndMessages(null); - postRequestHandler = null; - Util.castNonNull(requestHandlerThread).quit(); - requestHandlerThread = null; - mediaCrypto = null; - lastException = null; - currentKeyRequest = null; - currentProvisionRequest = null; - if (sessionId != null) { - mediaDrm.closeSession(sessionId); - sessionId = null; - eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionReleased); - } - releaseCallback.onSessionReleased(this); - } - } - public boolean hasSessionId(byte[] sessionId) { return Arrays.equals(this.sessionId, sessionId); } @@ -270,6 +235,42 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return offlineLicenseKeySetId; } + @Override + public void acquireReference() { + if (++referenceCount == 1) { + Assertions.checkState(state == STATE_OPENING); + requestHandlerThread = new HandlerThread("DrmRequestHandler"); + requestHandlerThread.start(); + postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); + if (openInternal(true)) { + doLicense(true); + } + } + } + + @Override + public void releaseReference() { + if (--referenceCount == 0) { + // Assigning null to various non-null variables for clean-up. + state = STATE_RELEASED; + Util.castNonNull(postResponseHandler).removeCallbacksAndMessages(null); + Util.castNonNull(postRequestHandler).removeCallbacksAndMessages(null); + postRequestHandler = null; + Util.castNonNull(requestHandlerThread).quit(); + requestHandlerThread = null; + mediaCrypto = null; + lastException = null; + currentKeyRequest = null; + currentProvisionRequest = null; + if (sessionId != null) { + mediaDrm.closeSession(sessionId); + sessionId = null; + eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionReleased); + } + releaseCallback.onSessionReleased(this); + } + } + // Internal methods. /** @@ -288,9 +289,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; try { sessionId = mediaDrm.openSession(); - eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionAcquired); mediaCrypto = mediaDrm.createMediaCrypto(sessionId); + eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionAcquired); state = STATE_OPENED; + Assertions.checkNotNull(sessionId); return true; } catch (NotProvisionedException e) { if (allowProvisioning) { @@ -329,6 +331,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @RequiresNonNull("sessionId") private void doLicense(boolean allowRetry) { + byte[] sessionId = Util.castNonNull(this.sessionId); switch (mode) { case DefaultDrmSessionManager.MODE_PLAYBACK: case DefaultDrmSessionManager.MODE_QUERY: @@ -364,6 +367,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; break; case DefaultDrmSessionManager.MODE_RELEASE: Assertions.checkNotNull(offlineLicenseKeySetId); + Assertions.checkNotNull(this.sessionId); // It's not necessary to restore the key (and open a session to do that) before releasing it // but this serves as a good sanity/fast-failure check. if (restoreKeys()) { 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 7481c60c64..84e984445a 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 @@ -432,19 +432,10 @@ public class DefaultDrmSessionManager initialDrmRequestRetryCount); sessions.add(session); } - session.acquire(); + session.acquireReference(); return session; } - @Override - public void releaseSession(DrmSession session) { - if (session instanceof ErrorStateDrmSession) { - // Do nothing. - return; - } - ((DefaultDrmSession) session).release(); - } - // ProvisioningManager implementation. @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index 392b0734b1..761fb74287 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -28,6 +28,20 @@ import java.util.Map; */ public interface DrmSession { + /** + * Invokes {@code newSession's} {@link #acquireReference()} and {@code previousSession's} {@link + * #releaseReference()} in that order. Does nothing for passed null values. + */ + static void replaceSessionReferences( + @Nullable DrmSession previousSession, @Nullable DrmSession newSession) { + if (newSession != null) { + newSession.acquireReference(); + } + if (previousSession != null) { + previousSession.releaseReference(); + } + } + /** * Wraps the throwable which is the cause of the error state. */ @@ -110,4 +124,18 @@ public interface DrmSession { */ @Nullable byte[] getOfflineLicenseKeySetId(); + + /** + * Increments the reference count for this session. A non-zero reference count session will keep + * any acquired resources. + */ + void acquireReference(); + + /** + * Decreases by one the reference count for this session. A session that reaches a zero reference + * count will release any resources it holds. + * + *

    The session must not be used after its reference count has been reduced to 0. + */ + void releaseReference(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java index d8093507a4..168783cf1c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java @@ -34,8 +34,10 @@ public interface DrmSessionManager { boolean canAcquireSession(DrmInitData drmInitData); /** - * Acquires a {@link DrmSession} for the specified {@link DrmInitData}. The {@link DrmSession} - * must be returned to {@link #releaseSession(DrmSession)} when it is no longer required. + * Returns a {@link DrmSession} with an acquired reference for the specified {@link DrmInitData}. + * + *

    The caller must call {@link DrmSession#releaseReference} to decrement the session's + * reference count when the session is no longer required. * * @param playbackLooper The looper associated with the media playback thread. * @param drmInitData DRM initialization data. All contained {@link SchemeData}s must contain @@ -43,10 +45,4 @@ public interface DrmSessionManager { * @return The DRM session. */ DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData); - - /** - * Releases a {@link DrmSession}. - */ - void releaseSession(DrmSession drmSession); - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java index bcc0739042..d40cf60906 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java @@ -57,4 +57,13 @@ public final class ErrorStateDrmSession implements Drm return null; } + @Override + public void acquireReference() { + // Do nothing. + } + + @Override + public void releaseReference() { + // Do nothing. + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 55a7a901ac..05dab7e42d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -235,7 +235,7 @@ public final class OfflineLicenseHelper { DrmSessionException error = drmSession.getError(); Pair licenseDurationRemainingSec = WidevineUtil.getLicenseDurationRemainingSec(drmSession); - drmSessionManager.releaseSession(drmSession); + drmSession.releaseReference(); if (error != null) { if (error.getCause() instanceof KeysExpiredException) { return Pair.create(0L, 0L); @@ -259,7 +259,7 @@ public final class OfflineLicenseHelper { drmInitData); DrmSessionException error = drmSession.getError(); byte[] keySetId = drmSession.getOfflineLicenseKeySetId(); - drmSessionManager.releaseSession(drmSession); + drmSession.releaseReference(); if (error != null) { throw error; } 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 cd043655ec..05f83109e8 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 @@ -941,21 +941,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } private void setSourceDrmSession(@Nullable DrmSession session) { - DrmSession previous = sourceDrmSession; + DrmSession.replaceSessionReferences(sourceDrmSession, session); sourceDrmSession = session; - releaseDrmSessionIfUnused(previous); } private void setCodecDrmSession(@Nullable DrmSession session) { - DrmSession previous = codecDrmSession; + DrmSession.replaceSessionReferences(codecDrmSession, session); codecDrmSession = session; - releaseDrmSessionIfUnused(previous); - } - - private void releaseDrmSessionIfUnused(@Nullable DrmSession session) { - if (session != null && session != sourceDrmSession && session != codecDrmSession) { - drmSessionManager.releaseSession(session); - } } /** @@ -1159,12 +1151,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } DrmSession session = drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); - if (session == sourceDrmSession || session == codecDrmSession) { - // We already had this session. The manager must be reference counting, so release it once - // to get the count attributed to this renderer back down to 1. - drmSessionManager.releaseSession(session); + if (sourceDrmSession != null) { + sourceDrmSession.releaseReference(); } - setSourceDrmSession(session); + sourceDrmSession = session; } else { setSourceDrmSession(null); } From c495a3f55e7a6ee1bfe653068967d7300d07acc0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 28 May 2019 16:36:09 +0100 Subject: [PATCH 1317/1556] Fix VP9 build setup Update configuration script to use an external build, so we can remove use of isysroot which is broken in the latest NDK r19c. Also switch from gnustl_static to c++_static so that ndk-build with NDK r19c succeeds. Issue: #5922 PiperOrigin-RevId: 250287551 --- extensions/vp9/README.md | 3 +- extensions/vp9/src/main/jni/Application.mk | 4 +- .../jni/generate_libvpx_android_configs.sh | 44 ++++++------------- 3 files changed, 18 insertions(+), 33 deletions(-) diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 2c5b64f8bd..be75eae359 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -29,6 +29,7 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" ``` * Download the [Android NDK][] and set its location in an environment variable. + The build configuration has been tested with Android NDK r19c. ``` NDK_PATH="" @@ -54,7 +55,7 @@ git checkout tags/v1.8.0 -b v1.8.0 ``` cd ${VP9_EXT_PATH}/jni && \ -./generate_libvpx_android_configs.sh "${NDK_PATH}" +./generate_libvpx_android_configs.sh ``` * Build the JNI native libraries from the command line: diff --git a/extensions/vp9/src/main/jni/Application.mk b/extensions/vp9/src/main/jni/Application.mk index 59bf5f8f87..ed28f07acb 100644 --- a/extensions/vp9/src/main/jni/Application.mk +++ b/extensions/vp9/src/main/jni/Application.mk @@ -15,6 +15,6 @@ # APP_OPTIM := release -APP_STL := gnustl_static +APP_STL := c++_static APP_CPPFLAGS := -frtti -APP_PLATFORM := android-9 +APP_PLATFORM := android-16 diff --git a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh index eab6862555..18f1dd5c69 100755 --- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh +++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh @@ -20,46 +20,33 @@ set -e -if [ $# -ne 1 ]; then - echo "Usage: ${0} " +if [ $# -ne 0 ]; then + echo "Usage: ${0}" exit fi -ndk="${1}" -shift 1 - # configuration parameters common to all architectures common_params="--disable-examples --disable-docs --enable-realtime-only" common_params+=" --disable-vp8 --disable-vp9-encoder --disable-webm-io" common_params+=" --disable-libyuv --disable-runtime-cpu-detect" +common_params+=" --enable-external-build" # configuration parameters for various architectures arch[0]="armeabi-v7a" -config[0]="--target=armv7-android-gcc --sdk-path=$ndk --enable-neon" -config[0]+=" --enable-neon-asm" +config[0]="--target=armv7-android-gcc --enable-neon --enable-neon-asm" -arch[1]="armeabi" -config[1]="--target=armv7-android-gcc --sdk-path=$ndk --disable-neon" -config[1]+=" --disable-neon-asm" +arch[1]="x86" +config[1]="--force-target=x86-android-gcc --disable-sse2" +config[1]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx" +config[1]+=" --disable-avx2 --enable-pic" -arch[2]="mips" -config[2]="--force-target=mips32-android-gcc --sdk-path=$ndk" +arch[2]="arm64-v8a" +config[2]="--force-target=armv8-android-gcc --enable-neon" -arch[3]="x86" -config[3]="--force-target=x86-android-gcc --sdk-path=$ndk --disable-sse2" +arch[3]="x86_64" +config[3]="--force-target=x86_64-android-gcc --disable-sse2" config[3]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx" -config[3]+=" --disable-avx2 --enable-pic" - -arch[4]="arm64-v8a" -config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --enable-neon" - -arch[5]="x86_64" -config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2" -config[5]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx" -config[5]+=" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm" - -arch[6]="mips64" -config[6]="--force-target=mips64-android-gcc --sdk-path=$ndk" +config[3]+=" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm" limit=$((${#arch[@]} - 1)) @@ -102,10 +89,7 @@ for i in $(seq 0 ${limit}); do # configure and make echo "build_android_configs: " echo "configure ${config[${i}]} ${common_params}" - ../../libvpx/configure ${config[${i}]} ${common_params} --extra-cflags=" \ - -isystem $ndk/sysroot/usr/include/arm-linux-androideabi \ - -isystem $ndk/sysroot/usr/include \ - " + ../../libvpx/configure ${config[${i}]} ${common_params} rm -f libvpx_srcs.txt for f in ${allowed_files}; do # the build system supports multiple different configurations. avoid From 04f38885501f8f6091c35d7a091e6dd43ded95c8 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 28 May 2019 17:26:19 +0100 Subject: [PATCH 1318/1556] Add allowOnlyClearBuffers to SampleQueue#read Removes the need for duplicate calls to SampleQueue#read when implementing DecryptionResources acquisition in the MediaSources. PiperOrigin-RevId: 250298175 --- .../source/ProgressiveMediaPeriod.java | 7 +- .../source/SampleMetadataQueue.java | 7 ++ .../exoplayer2/source/SampleQueue.java | 14 ++- .../source/chunk/ChunkSampleStream.java | 14 ++- .../exoplayer2/source/SampleQueueTest.java | 114 ++++++++++++++++-- .../source/dash/PlayerEmsgHandler.java | 9 +- .../source/hls/HlsSampleStreamWrapper.java | 7 +- 7 files changed, 154 insertions(+), 18 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index e8f630f202..dbf5f8aa5d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -447,7 +447,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; maybeNotifyDownstreamFormat(track); int result = sampleQueues[track].read( - formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); + formatHolder, + buffer, + formatRequired, + /* allowOnlyClearBuffers= */ false, + loadingFinished, + lastSeekPositionUs); if (result == C.RESULT_NOTHING_READ) { maybeStartDeferredRetry(track); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index 25cc73d4ae..b2c09bd70f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -230,6 +230,8 @@ import com.google.android.exoplayer2.util.Util; * @param formatRequired Whether the caller requires that the format of the stream be read even if * it's not changing. A sample will never be read if set to true, however it is still possible * for the end of stream or nothing to be read. + * @param allowOnlyClearBuffers If set to true, this method will not return encrypted buffers, + * returning {@link C#RESULT_NOTHING_READ} (without advancing the read position) instead. * @param loadingFinished True if an empty queue should be considered the end of the stream. * @param downstreamFormat The current downstream {@link Format}. If the format of the next sample * is different to the current downstream format then a format will be read. @@ -242,6 +244,7 @@ import com.google.android.exoplayer2.util.Util; FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, + boolean allowOnlyClearBuffers, boolean loadingFinished, Format downstreamFormat, SampleExtrasHolder extrasHolder) { @@ -264,6 +267,10 @@ import com.google.android.exoplayer2.util.Util; return C.RESULT_FORMAT_READ; } + if (allowOnlyClearBuffers && (flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0) { + return C.RESULT_NOTHING_READ; + } + buffer.setFlags(flags[relativeReadIndex]); buffer.timeUs = timesUs[relativeReadIndex]; if (buffer.isFlagsOnly()) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index e8f4953436..976a5d4e48 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -324,6 +324,8 @@ public class SampleQueue implements TrackOutput { * @param formatRequired Whether the caller requires that the format of the stream be read even if * it's not changing. A sample will never be read if set to true, however it is still possible * for the end of stream or nothing to be read. + * @param allowOnlyClearBuffers If set to true, this method will not return encrypted buffers, + * returning {@link C#RESULT_NOTHING_READ} (without advancing the read position) instead. * @param loadingFinished True if an empty queue should be considered the end of the stream. * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will * be set if the buffer's timestamp is less than this value. @@ -334,10 +336,18 @@ public class SampleQueue implements TrackOutput { FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, + boolean allowOnlyClearBuffers, boolean loadingFinished, long decodeOnlyUntilUs) { - int result = metadataQueue.read(formatHolder, buffer, formatRequired, loadingFinished, - downstreamFormat, extrasHolder); + int result = + metadataQueue.read( + formatHolder, + buffer, + formatRequired, + allowOnlyClearBuffers, + loadingFinished, + downstreamFormat, + extrasHolder); switch (result) { case C.RESULT_FORMAT_READ: downstreamFormat = formatHolder.format; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index d7a19fa9d4..d9b28d9c92 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -409,7 +409,12 @@ public class ChunkSampleStream implements SampleStream, S } maybeNotifyPrimaryTrackFormatChanged(); return primarySampleQueue.read( - formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); + formatHolder, + buffer, + formatRequired, + /* allowOnlyClearBuffers= */ false, + loadingFinished, + decodeOnlyUntilPositionUs); } @Override @@ -801,7 +806,12 @@ public class ChunkSampleStream implements SampleStream, S } maybeNotifyDownstreamFormat(); return sampleQueue.read( - formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); + formatHolder, + buffer, + formatRequired, + /* allowOnlyClearBuffers= */ false, + loadingFinished, + decodeOnlyUntilPositionUs); } public void release() { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 450f0ecd3a..bfc6bb52c9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -29,10 +29,12 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.Arrays; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -89,6 +91,8 @@ public final class SampleQueueTest { private static final Format[] SAMPLE_FORMATS = new Format[] {FORMAT_1, FORMAT_1, FORMAT_1, FORMAT_1, FORMAT_2, FORMAT_2, FORMAT_2, FORMAT_2}; private static final int DATA_SECOND_KEYFRAME_INDEX = 4; + private static final TrackOutput.CryptoData DUMMY_CRYPTO_DATA = + new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, new byte[16], 0, 0); private Allocator allocator; private SampleQueue sampleQueue; @@ -511,6 +515,49 @@ public final class SampleQueueTest { assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(MIN_VALUE); } + @Test + public void testAllowOnlyClearBuffers() { + int[] flags = + new int[] { + C.BUFFER_FLAG_KEY_FRAME, + C.BUFFER_FLAG_ENCRYPTED, + 0, + 0, + 0, + C.BUFFER_FLAG_KEY_FRAME | C.BUFFER_FLAG_ENCRYPTED, + 0, + 0 + }; + int[] sampleSizes = new int[flags.length]; + Arrays.fill(sampleSizes, /* val= */ 1); + + // Two encryption preamble bytes per encrypted sample in the sample queue. + byte[] sampleData = new byte[flags.length + 2 + 2]; + Arrays.fill(sampleData, /* val= */ (byte) 1); + + writeTestData( + sampleData, sampleSizes, new int[flags.length], SAMPLE_TIMESTAMPS, SAMPLE_FORMATS, flags); + assertReadFormat(/* formatRequired= */ false, FORMAT_1); + assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); + assertResult(RESULT_NOTHING_READ, /* allowOnlyClearBuffers= */ true); + + assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ false); + + assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); + assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); + assertResult(RESULT_FORMAT_READ, /* allowOnlyClearBuffers= */ true); + assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); + assertResult(RESULT_NOTHING_READ, /* allowOnlyClearBuffers= */ true); + + assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ false); + + assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); + assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); + assertResult(RESULT_NOTHING_READ, /* allowOnlyClearBuffers= */ true); + + assertResult(RESULT_NOTHING_READ, /* allowOnlyClearBuffers= */ false); + } + @Test public void testLargestQueuedTimestampWithRead() { writeTestData(); @@ -602,8 +649,12 @@ public final class SampleQueueTest { sampleQueue.format(sampleFormats[i]); format = sampleFormats[i]; } - sampleQueue.sampleMetadata(sampleTimestamps[i], sampleFlags[i], sampleSizes[i], - sampleOffsets[i], null); + sampleQueue.sampleMetadata( + sampleTimestamps[i], + sampleFlags[i], + sampleSizes[i], + sampleOffsets[i], + (sampleFlags[i] & C.BUFFER_FLAG_ENCRYPTED) != 0 ? DUMMY_CRYPTO_DATA : null); } } @@ -714,11 +765,18 @@ public final class SampleQueueTest { /** * Asserts {@link SampleQueue#read} returns {@link C#RESULT_NOTHING_READ}. * - * @param formatRequired The value of {@code formatRequired} passed to readData. + * @param formatRequired The value of {@code formatRequired} passed to {@link SampleQueue#read}. */ private void assertReadNothing(boolean formatRequired) { clearFormatHolderAndInputBuffer(); - int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0); + int result = + sampleQueue.read( + formatHolder, + inputBuffer, + formatRequired, + /* allowOnlyClearBuffers= */ false, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); assertThat(result).isEqualTo(RESULT_NOTHING_READ); // formatHolder should not be populated. assertThat(formatHolder.format).isNull(); @@ -728,14 +786,21 @@ public final class SampleQueueTest { } /** - * Asserts {@link SampleQueue#read} returns {@link C#RESULT_BUFFER_READ} and that the - * {@link DecoderInputBuffer#isEndOfStream()} is set. + * Asserts {@link SampleQueue#read} returns {@link C#RESULT_BUFFER_READ} and that the {@link + * DecoderInputBuffer#isEndOfStream()} is set. * - * @param formatRequired The value of {@code formatRequired} passed to readData. + * @param formatRequired The value of {@code formatRequired} passed to {@link SampleQueue#read}. */ private void assertReadEndOfStream(boolean formatRequired) { clearFormatHolderAndInputBuffer(); - int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, true, 0); + int result = + sampleQueue.read( + formatHolder, + inputBuffer, + formatRequired, + /* allowOnlyClearBuffers= */ false, + /* loadingFinished= */ true, + /* decodeOnlyUntilUs= */ 0); assertThat(result).isEqualTo(RESULT_BUFFER_READ); // formatHolder should not be populated. assertThat(formatHolder.format).isNull(); @@ -750,12 +815,19 @@ public final class SampleQueueTest { * Asserts {@link SampleQueue#read} returns {@link C#RESULT_FORMAT_READ} and that the format * holder is filled with a {@link Format} that equals {@code format}. * - * @param formatRequired The value of {@code formatRequired} passed to readData. + * @param formatRequired The value of {@code formatRequired} passed to {@link SampleQueue#read}. * @param format The expected format. */ private void assertReadFormat(boolean formatRequired, Format format) { clearFormatHolderAndInputBuffer(); - int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0); + int result = + sampleQueue.read( + formatHolder, + inputBuffer, + formatRequired, + /* allowOnlyClearBuffers= */ false, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); assertThat(result).isEqualTo(RESULT_FORMAT_READ); // formatHolder should be populated. assertThat(formatHolder.format).isEqualTo(format); @@ -777,7 +849,14 @@ public final class SampleQueueTest { private void assertReadSample( long timeUs, boolean isKeyframe, byte[] sampleData, int offset, int length) { clearFormatHolderAndInputBuffer(); - int result = sampleQueue.read(formatHolder, inputBuffer, false, false, 0); + int result = + sampleQueue.read( + formatHolder, + inputBuffer, + /* formatRequired= */ false, + /* allowOnlyClearBuffers= */ false, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); assertThat(result).isEqualTo(RESULT_BUFFER_READ); // formatHolder should not be populated. assertThat(formatHolder.format).isNull(); @@ -793,6 +872,19 @@ public final class SampleQueueTest { assertThat(readData).isEqualTo(copyOfRange(sampleData, offset, offset + length)); } + /** Asserts {@link SampleQueue#read} returns the given result. */ + private void assertResult(int expectedResult, boolean allowOnlyClearBuffers) { + int obtainedResult = + sampleQueue.read( + formatHolder, + inputBuffer, + /* formatRequired= */ false, + allowOnlyClearBuffers, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); + assertThat(obtainedResult).isEqualTo(expectedResult); + } + /** * Asserts the number of allocations currently in use by {@code sampleQueue}. * diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java index 34e1ecc2b6..af4bf3ad70 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java @@ -371,7 +371,14 @@ public final class PlayerEmsgHandler implements Handler.Callback { @Nullable private MetadataInputBuffer dequeueSample() { buffer.clear(); - int result = sampleQueue.read(formatHolder, buffer, false, false, 0); + int result = + sampleQueue.read( + formatHolder, + buffer, + /* formatRequired= */ false, + /* allowOnlyClearBuffers= */ false, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); if (result == C.RESULT_BUFFER_READ) { buffer.flip(); return buffer; 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 434b6c2011..96704053cb 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 @@ -491,7 +491,12 @@ import java.util.Map; int result = sampleQueues[sampleQueueIndex].read( - formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs); + formatHolder, + buffer, + requireFormat, + /* allowOnlyClearBuffers= */ false, + loadingFinished, + lastSeekPositionUs); if (result == C.RESULT_FORMAT_READ) { Format format = formatHolder.format; if (sampleQueueIndex == primarySampleQueueIndex) { From 90325c699e735bcd80b92ed6d7386ce75585f8b8 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 28 May 2019 17:40:50 +0100 Subject: [PATCH 1319/1556] Allow enabling decoder fallback in DefaultRenderersFactory Also allow enabling decoder fallback with MediaCodecAudioRenderer. Issue: #5942 PiperOrigin-RevId: 250301422 --- RELEASENOTES.md | 2 + .../exoplayer2/DefaultRenderersFactory.java | 30 +++++++++++++- .../audio/MediaCodecAudioRenderer.java | 40 ++++++++++++++++++- .../testutil/DebugRenderersFactory.java | 1 + 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index deda085f40..edddbbe7ec 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -33,6 +33,8 @@ * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the preparation of the `DownloadHelper` failed ([#5915](https://github.com/google/ExoPlayer/issues/5915)). +* Allow enabling decoder fallback with `DefaultRenderersFactory` + ([#5942](https://github.com/google/ExoPlayer/issues/5942)). ### 2.10.1 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java index 2a977f5bba..490d961396 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -24,6 +24,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.DefaultAudioSink; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; @@ -90,6 +91,7 @@ public class DefaultRenderersFactory implements RenderersFactory { @ExtensionRendererMode private int extensionRendererMode; private long allowedVideoJoiningTimeMs; private boolean playClearSamplesWithoutKeys; + private boolean enableDecoderFallback; private MediaCodecSelector mediaCodecSelector; /** @param context A {@link Context}. */ @@ -202,6 +204,19 @@ public class DefaultRenderersFactory implements RenderersFactory { return this; } + /** + * Sets whether to enable fallback to lower-priority decoders if decoder initialization fails. + * This may result in using a decoder that is less efficient or slower than the primary decoder. + * + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory setEnableDecoderFallback(boolean enableDecoderFallback) { + this.enableDecoderFallback = enableDecoderFallback; + return this; + } + /** * Sets a {@link MediaCodecSelector} for use by {@link MediaCodec} based renderers. * @@ -248,6 +263,7 @@ public class DefaultRenderersFactory implements RenderersFactory { mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, + enableDecoderFallback, eventHandler, videoRendererEventListener, allowedVideoJoiningTimeMs, @@ -258,6 +274,7 @@ public class DefaultRenderersFactory implements RenderersFactory { mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, + enableDecoderFallback, buildAudioProcessors(), eventHandler, audioRendererEventListener, @@ -282,6 +299,9 @@ public class DefaultRenderersFactory implements RenderersFactory { * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of * the media. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is slower/less efficient than + * the primary decoder. * @param eventHandler A handler associated with the main thread's looper. * @param eventListener An event listener. * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to @@ -294,6 +314,7 @@ public class DefaultRenderersFactory implements RenderersFactory { MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, Handler eventHandler, VideoRendererEventListener eventListener, long allowedVideoJoiningTimeMs, @@ -305,6 +326,7 @@ public class DefaultRenderersFactory implements RenderersFactory { allowedVideoJoiningTimeMs, drmSessionManager, playClearSamplesWithoutKeys, + enableDecoderFallback, eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); @@ -356,6 +378,9 @@ public class DefaultRenderersFactory implements RenderersFactory { * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of * the media. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is slower/less efficient than + * the primary decoder. * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers * before output. May be empty. * @param eventHandler A handler to use when invoking event listeners and outputs. @@ -368,6 +393,7 @@ public class DefaultRenderersFactory implements RenderersFactory { MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, AudioProcessor[] audioProcessors, Handler eventHandler, AudioRendererEventListener eventListener, @@ -378,10 +404,10 @@ public class DefaultRenderersFactory implements RenderersFactory { mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, + enableDecoderFallback, eventHandler, eventListener, - AudioCapabilities.getCapabilities(context), - audioProcessors)); + new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors))); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; 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 c3ec759c2d..d83e32c61c 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 @@ -246,12 +246,50 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) { + this( + context, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + /* enableDecoderFallback= */ false, + eventHandler, + eventListener, + audioSink); + } + + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is slower/less efficient than + * the primary decoder. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioSink The sink to which audio will be output. + */ + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + AudioSink audioSink) { super( C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, - /* enableDecoderFallback= */ false, + enableDecoderFallback, /* assumedMinimumCodecOperatingRate= */ 44100); this.context = context.getApplicationContext(); this.audioSink = audioSink; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index 2b479c549a..6bd4c8dd14 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -56,6 +56,7 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, Handler eventHandler, VideoRendererEventListener eventListener, long allowedVideoJoiningTimeMs, From 8a0fb6b78f64005fe0d1403e0ad42b601a221bd9 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 29 May 2019 10:09:54 +0100 Subject: [PATCH 1320/1556] Fix video size reporting in surface YUV mode In surface YUV output mode the width/height fields of the VpxOutputBuffer were never populated. Fix this by adding a new method to set the width/height and calling it from JNI like we do for GL YUV mode. PiperOrigin-RevId: 250449734 --- .../android/exoplayer2/ext/vp9/VpxOutputBuffer.java | 13 +++++++++++-- extensions/vp9/src/main/jni/vpx_jni.cc | 7 +++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java index 22330e0a05..30d7b8e92c 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java @@ -60,8 +60,8 @@ public final class VpxOutputBuffer extends OutputBuffer { * Initializes the buffer. * * @param timeUs The presentation timestamp for the buffer, in microseconds. - * @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE} and {@link - * VpxDecoder#OUTPUT_MODE_YUV}. + * @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE}, {@link + * VpxDecoder#OUTPUT_MODE_YUV} and {@link VpxDecoder#OUTPUT_MODE_SURFACE_YUV}. */ public void init(long timeUs, int mode) { this.timeUs = timeUs; @@ -110,6 +110,15 @@ public final class VpxOutputBuffer extends OutputBuffer { return true; } + /** + * Configures the buffer for the given frame dimensions when passing actual frame data via {@link + * #decoderPrivate}. Called via JNI after decoding completes. + */ + public void initForPrivateFrame(int width, int height) { + this.width = width; + this.height = height; + } + private void initData(int size) { if (data == null || data.capacity() < size) { data = ByteBuffer.allocateDirect(size); diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index 82c023afbc..9fc8b09a18 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -60,6 +60,7 @@ // JNI references for VpxOutputBuffer class. static jmethodID initForYuvFrame; +static jmethodID initForPrivateFrame; static jfieldID dataField; static jfieldID outputModeField; static jfieldID decoderPrivateField; @@ -481,6 +482,8 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter, "com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer"); initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame", "(IIIII)Z"); + initForPrivateFrame = + env->GetMethodID(outputBufferClass, "initForPrivateFrame", "(II)V"); dataField = env->GetFieldID(outputBufferClass, "data", "Ljava/nio/ByteBuffer;"); outputModeField = env->GetFieldID(outputBufferClass, "mode", "I"); @@ -602,6 +605,10 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { } jfb->d_w = img->d_w; jfb->d_h = img->d_h; + env->CallVoidMethod(jOutputBuffer, initForPrivateFrame, img->d_w, img->d_h); + if (env->ExceptionCheck()) { + return -1; + } env->SetIntField(jOutputBuffer, decoderPrivateField, id + kDecoderPrivateBase); } From 8912db5ed9df8624ce8574f99b3b2972193a7656 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 29 May 2019 10:12:27 +0100 Subject: [PATCH 1321/1556] Remove DecryptionResource Reference count was built into DrmSession PiperOrigin-RevId: 250449988 --- .../exoplayer2/drm/DecryptionResource.java | 72 ------------------- 1 file changed, 72 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionResource.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionResource.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionResource.java deleted file mode 100644 index dbe5c93172..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionResource.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2019 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.drm; - -/** - * A reference-counted resource used in the decryption of media samples. - * - * @param The reference type with which to make {@link Owner#onLastReferenceReleased} calls. - * Subclasses are expected to pass themselves. - */ -public abstract class DecryptionResource> { - - /** - * Implemented by the class in charge of managing a {@link DecryptionResource resource's} - * lifecycle. - */ - public interface Owner> { - - /** - * Called when the last reference to a {@link DecryptionResource} is {@link #releaseReference() - * released}. - */ - void onLastReferenceReleased(T resource); - } - - // TODO: Consider adding a handler on which the owner should be called. - private final DecryptionResource.Owner owner; - private int referenceCount; - - /** - * Creates a new instance with reference count zero. - * - * @param owner The owner of this instance. - */ - public DecryptionResource(Owner owner) { - this.owner = owner; - referenceCount = 0; - } - - /** Increases by one the reference count for this resource. */ - public void acquireReference() { - referenceCount++; - } - - /** - * Decreases by one the reference count for this resource, and notifies the owner if said count - * reached zero as a result of this operation. - * - *

    Must only be called as releasing counter-part of {@link #acquireReference()}. - */ - @SuppressWarnings("unchecked") - public void releaseReference() { - if (--referenceCount == 0) { - owner.onLastReferenceReleased((T) this); - } else if (referenceCount < 0) { - throw new IllegalStateException("Illegal release of resource."); - } - } -} From 94d668567c27e66fc9441f5fe1eccd42e83bbd2c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 May 2019 11:26:08 +0100 Subject: [PATCH 1322/1556] Migrate org.mockito.Matchers#any* to org.mockito.ArgumentMatchers The former is deprecated and replaced by the latter in Mockito 2. However, there is a functional difference: ArgumentMatchers will reject `null` and check the type if the matcher specified a type (e.g. `any(Class)` or `anyInt()`). `any()` will remain to accept anything. PiperOrigin-RevId: 250458607 --- .../android/exoplayer2/ext/cronet/CronetDataSourceTest.java | 4 ++-- .../android/exoplayer2/drm/OfflineLicenseHelperTest.java | 2 +- .../exoplayer2/trackselection/AdaptiveTrackSelectionTest.java | 2 +- .../exoplayer2/upstream/cache/CachedRegionTrackerTest.java | 4 ++-- .../android/exoplayer2/source/hls/HlsMediaPeriodTest.java | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index df36076899..a01c5e84b6 100644 --- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.cronet; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index d6b0b5ba15..886be4c476 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -17,9 +17,9 @@ package com.google.android.exoplayer2.drm; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; import android.util.Pair; 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 b077a92d99..91e7393fe7 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 @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.trackselection; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java index b00ee73f0f..73780f56f3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.upstream.cache; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; import androidx.test.core.app.ApplicationProvider; diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java index dc9c0e0644..f389944670 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer2.source.hls; -import static org.mockito.Matchers.anyInt; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; From 09b00c7fb06253d6171a6098ad4bdb82a5a7c9d7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 29 May 2019 18:25:27 +0100 Subject: [PATCH 1323/1556] Allow passthrough of E-AC3-JOC streams PiperOrigin-RevId: 250517338 --- .../java/com/google/android/exoplayer2/C.java | 7 +++-- .../exoplayer2/audio/DefaultAudioSink.java | 3 +- .../audio/MediaCodecAudioRenderer.java | 31 +++++++++++++++++-- .../android/exoplayer2/util/MimeTypes.java | 3 +- 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index afe6a9879b..8ded5038b0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -146,8 +146,8 @@ public final class C { * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link * #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link * #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_AC3}, {@link - * #ENCODING_E_AC3}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or - * {@link #ENCODING_DOLBY_TRUEHD}. + * #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, + * {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -163,6 +163,7 @@ public final class C { ENCODING_PCM_A_LAW, ENCODING_AC3, ENCODING_E_AC3, + ENCODING_E_AC3_JOC, ENCODING_AC4, ENCODING_DTS, ENCODING_DTS_HD, @@ -210,6 +211,8 @@ public final class C { public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; /** @see AudioFormat#ENCODING_E_AC3 */ public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3; + /** @see AudioFormat#ENCODING_E_AC3_JOC */ + public static final int ENCODING_E_AC3_JOC = AudioFormat.ENCODING_E_AC3_JOC; /** @see AudioFormat#ENCODING_AC4 */ public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4; /** @see AudioFormat#ENCODING_DTS */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index cf914567d6..425b0994b6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -1125,6 +1125,7 @@ public final class DefaultAudioSink implements AudioSink { case C.ENCODING_AC3: return 640 * 1000 / 8; case C.ENCODING_E_AC3: + case C.ENCODING_E_AC3_JOC: return 6144 * 1000 / 8; case C.ENCODING_AC4: return 2688 * 1000 / 8; @@ -1154,7 +1155,7 @@ public final class DefaultAudioSink implements AudioSink { return DtsUtil.parseDtsAudioSampleCount(buffer); } else if (encoding == C.ENCODING_AC3) { return Ac3Util.getAc3SyncframeAudioSampleCount(); - } else if (encoding == C.ENCODING_E_AC3) { + } else if (encoding == C.ENCODING_E_AC3 || encoding == C.ENCODING_E_AC3_JOC) { return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer); } else if (encoding == C.ENCODING_AC4) { return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer); 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 d83e32c61c..fe8e898b06 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 @@ -374,7 +374,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @return Whether passthrough playback is supported. */ protected boolean allowPassthrough(int channelCount, String mimeType) { - return audioSink.supportsOutput(channelCount, MimeTypes.getEncoding(mimeType)); + return getPassthroughEncoding(channelCount, mimeType) != C.ENCODING_INVALID; } @Override @@ -471,11 +471,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @C.Encoding int encoding; MediaFormat format; if (passthroughMediaFormat != null) { - encoding = MimeTypes.getEncoding(passthroughMediaFormat.getString(MediaFormat.KEY_MIME)); format = passthroughMediaFormat; + encoding = + getPassthroughEncoding( + format.getInteger(MediaFormat.KEY_CHANNEL_COUNT), + format.getString(MediaFormat.KEY_MIME)); } else { - encoding = pcmEncoding; format = outputFormat; + encoding = pcmEncoding; } int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); @@ -497,6 +500,28 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } + /** + * Returns the {@link C.Encoding} constant to use for passthrough of the given format, or {@link + * C#ENCODING_INVALID} if passthrough is not possible. + */ + @C.Encoding + protected int getPassthroughEncoding(int channelCount, String mimeType) { + if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) { + if (audioSink.supportsOutput(channelCount, C.ENCODING_E_AC3_JOC)) { + return MimeTypes.getEncoding(MimeTypes.AUDIO_E_AC3_JOC); + } + // E-AC3 receivers can decode JOC streams, but in 2-D rather than 3-D, so try to fall back. + mimeType = MimeTypes.AUDIO_E_AC3; + } + + @C.Encoding int encoding = MimeTypes.getEncoding(mimeType); + if (audioSink.supportsOutput(channelCount, encoding)) { + return encoding; + } else { + return C.ENCODING_INVALID; + } + } + /** * Called when the audio session id becomes known. The default implementation is a no-op. One * reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in 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 e603f76dbc..61457c308d 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 @@ -348,8 +348,9 @@ public final class MimeTypes { case MimeTypes.AUDIO_AC3: return C.ENCODING_AC3; case MimeTypes.AUDIO_E_AC3: - case MimeTypes.AUDIO_E_AC3_JOC: return C.ENCODING_E_AC3; + case MimeTypes.AUDIO_E_AC3_JOC: + return C.ENCODING_E_AC3_JOC; case MimeTypes.AUDIO_AC4: return C.ENCODING_AC4; case MimeTypes.AUDIO_DTS: From 1151848f590bd3a68f2e2d96395903789c0dbd53 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 May 2019 18:33:25 +0100 Subject: [PATCH 1324/1556] No-op move of span touching into helper method PiperOrigin-RevId: 250519114 --- .../upstream/cache/SimpleCache.java | 116 ++++++++++-------- 1 file changed, 63 insertions(+), 53 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index c53c4337b5..38c43bd551 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -408,30 +408,9 @@ public final class SimpleCache implements Cache { SimpleCacheSpan span = getSpan(key, position); - // Read case. if (span.isCached) { - if (!touchCacheSpans) { - return span; - } - String fileName = Assertions.checkNotNull(span.file).getName(); - long length = span.length; - long lastTouchTimestamp = System.currentTimeMillis(); - boolean updateFile = false; - if (fileIndex != null) { - try { - fileIndex.set(fileName, length, lastTouchTimestamp); - } catch (IOException e) { - Log.w(TAG, "Failed to update index with new touch timestamp."); - } - } else { - // Updating the file itself to incorporate the new last touch timestamp is much slower than - // updating the file index. Hence we only update the file if we don't have a file index. - updateFile = true; - } - SimpleCacheSpan newSpan = - contentIndex.get(key).setLastTouchTimestamp(span, lastTouchTimestamp, updateFile); - notifySpanTouched(span, newSpan); - return newSpan; + // Read case. + return touchSpan(key, span); } CachedContent cachedContent = contentIndex.getOrAdd(key); @@ -558,36 +537,6 @@ public final class SimpleCache implements Cache { return contentIndex.getContentMetadata(key); } - /** - * Returns the cache {@link SimpleCacheSpan} corresponding to the provided lookup {@link - * SimpleCacheSpan}. - * - *

    If the lookup position is contained by an existing entry in the cache, then the returned - * {@link SimpleCacheSpan} defines the file in which the data is stored. If the lookup position is - * not contained by an existing entry, then the returned {@link SimpleCacheSpan} defines the - * maximum extents of the hole in the cache. - * - * @param key The key of the span being requested. - * @param position The position of the span being requested. - * @return The corresponding cache {@link SimpleCacheSpan}. - */ - private SimpleCacheSpan getSpan(String key, long position) { - CachedContent cachedContent = contentIndex.get(key); - if (cachedContent == null) { - return SimpleCacheSpan.createOpenHole(key, position); - } - while (true) { - SimpleCacheSpan span = cachedContent.getSpan(position); - if (span.isCached && !span.file.exists()) { - // The file has been deleted from under us. It's likely that other files will have been - // deleted too, so scan the whole in-memory representation. - removeStaleSpans(); - continue; - } - return span; - } - } - /** Ensures that the cache's in-memory representation has been initialized. */ private void initialize() { if (!cacheDir.exists()) { @@ -696,6 +645,67 @@ public final class SimpleCache implements Cache { } } + /** + * Touches a cache span, returning the updated result. If the evictor does not require cache spans + * to be touched, then this method does nothing and the span is returned without modification. + * + * @param key The key of the span being touched. + * @param span The span being touched. + * @return The updated span. + */ + private SimpleCacheSpan touchSpan(String key, SimpleCacheSpan span) { + if (!touchCacheSpans) { + return span; + } + String fileName = Assertions.checkNotNull(span.file).getName(); + long length = span.length; + long lastTouchTimestamp = System.currentTimeMillis(); + boolean updateFile = false; + if (fileIndex != null) { + try { + fileIndex.set(fileName, length, lastTouchTimestamp); + } catch (IOException e) { + Log.w(TAG, "Failed to update index with new touch timestamp."); + } + } else { + // Updating the file itself to incorporate the new last touch timestamp is much slower than + // updating the file index. Hence we only update the file if we don't have a file index. + updateFile = true; + } + SimpleCacheSpan newSpan = + contentIndex.get(key).setLastTouchTimestamp(span, lastTouchTimestamp, updateFile); + notifySpanTouched(span, newSpan); + return newSpan; + } + + /** + * Returns the cache span corresponding to the provided lookup span. + * + *

    If the lookup position is contained by an existing entry in the cache, then the returned + * span defines the file in which the data is stored. If the lookup position is not contained by + * an existing entry, then the returned span defines the maximum extents of the hole in the cache. + * + * @param key The key of the span being requested. + * @param position The position of the span being requested. + * @return The corresponding cache {@link SimpleCacheSpan}. + */ + private SimpleCacheSpan getSpan(String key, long position) { + CachedContent cachedContent = contentIndex.get(key); + if (cachedContent == null) { + return SimpleCacheSpan.createOpenHole(key, position); + } + while (true) { + SimpleCacheSpan span = cachedContent.getSpan(position); + if (span.isCached && !span.file.exists()) { + // The file has been deleted from under us. It's likely that other files will have been + // deleted too, so scan the whole in-memory representation. + removeStaleSpans(); + continue; + } + return span; + } + } + /** * Adds a cached span to the in-memory representation. * From 71418f9411daa0f131091c18f480132fef8ad061 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 29 May 2019 18:36:01 +0100 Subject: [PATCH 1325/1556] Fix TTML bitmap subtitles + Use start for anchoring, instead of center. + Add the height to the TTML bitmap cue rendering layout. Issue:#5633 PiperOrigin-RevId: 250519710 --- RELEASENOTES.md | 7 +++++-- .../android/exoplayer2/text/ttml/TtmlDecoder.java | 1 + .../google/android/exoplayer2/text/ttml/TtmlNode.java | 4 ++-- .../android/exoplayer2/text/ttml/TtmlRegion.java | 4 ++++ .../android/exoplayer2/text/ttml/TtmlDecoderTest.java | 10 +++++----- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index edddbbe7ec..3b1ccc3d43 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,8 +9,11 @@ checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Decoders: Prefer decoders that advertise format support over ones that do not, even if they are listed lower in the `MediaCodecList`. -* CEA-608: Handle XDS and TEXT modes - ([5807](https://github.com/google/ExoPlayer/pull/5807)). +* Subtitles: + * CEA-608: Handle XDS and TEXT modes + ([#5807](https://github.com/google/ExoPlayer/pull/5807)). + * TTML: Fix bitmap rendering + ([#5633](https://github.com/google/ExoPlayer/pull/5633)). * Audio: * Fix an issue where not all audio was played out when the configuration for the underlying track was changing (e.g., at some period transitions). 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 b39f467968..6e0c495466 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 @@ -429,6 +429,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { /* lineType= */ Cue.LINE_TYPE_FRACTION, lineAnchor, width, + height, /* textSizeType= */ Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING, /* textSize= */ regionTextHeight); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java index ecf5c8b0a0..3b4d061aaa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java @@ -231,11 +231,11 @@ import java.util.TreeSet; new Cue( bitmap, region.position, - Cue.ANCHOR_TYPE_MIDDLE, + Cue.ANCHOR_TYPE_START, region.line, region.lineAnchor, region.width, - /* height= */ Cue.DIMEN_UNSET)); + region.height)); } // Create text based cues. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java index 2b1e9cf99a..3cbc25d4b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.text.Cue; public final @Cue.LineType int lineType; public final @Cue.AnchorType int lineAnchor; public final float width; + public final float height; public final @Cue.TextSizeType int textSizeType; public final float textSize; @@ -39,6 +40,7 @@ import com.google.android.exoplayer2.text.Cue; /* lineType= */ Cue.TYPE_UNSET, /* lineAnchor= */ Cue.TYPE_UNSET, /* width= */ Cue.DIMEN_UNSET, + /* height= */ Cue.DIMEN_UNSET, /* textSizeType= */ Cue.TYPE_UNSET, /* textSize= */ Cue.DIMEN_UNSET); } @@ -50,6 +52,7 @@ import com.google.android.exoplayer2.text.Cue; @Cue.LineType int lineType, @Cue.AnchorType int lineAnchor, float width, + float height, int textSizeType, float textSize) { this.id = id; @@ -58,6 +61,7 @@ import com.google.android.exoplayer2.text.Cue; this.lineType = lineType; this.lineAnchor = lineAnchor; this.width = width; + this.height = height; this.textSizeType = textSizeType; this.textSize = textSize; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java index 000d0634ce..85af6482c0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java @@ -514,7 +514,7 @@ public final class TtmlDecoderTest { assertThat(cue.position).isEqualTo(24f / 100f); assertThat(cue.line).isEqualTo(28f / 100f); assertThat(cue.size).isEqualTo(51f / 100f); - assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET); + assertThat(cue.bitmapHeight).isEqualTo(12f / 100f); cues = subtitle.getCues(4000000); assertThat(cues).hasSize(1); @@ -524,7 +524,7 @@ public final class TtmlDecoderTest { assertThat(cue.position).isEqualTo(21f / 100f); assertThat(cue.line).isEqualTo(35f / 100f); assertThat(cue.size).isEqualTo(57f / 100f); - assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET); + assertThat(cue.bitmapHeight).isEqualTo(6f / 100f); cues = subtitle.getCues(7500000); assertThat(cues).hasSize(1); @@ -534,7 +534,7 @@ public final class TtmlDecoderTest { assertThat(cue.position).isEqualTo(24f / 100f); assertThat(cue.line).isEqualTo(28f / 100f); assertThat(cue.size).isEqualTo(51f / 100f); - assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET); + assertThat(cue.bitmapHeight).isEqualTo(12f / 100f); } @Test @@ -549,7 +549,7 @@ public final class TtmlDecoderTest { assertThat(cue.position).isEqualTo(307f / 1280f); assertThat(cue.line).isEqualTo(562f / 720f); assertThat(cue.size).isEqualTo(653f / 1280f); - assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET); + assertThat(cue.bitmapHeight).isEqualTo(86f / 720f); cues = subtitle.getCues(4000000); assertThat(cues).hasSize(1); @@ -559,7 +559,7 @@ public final class TtmlDecoderTest { assertThat(cue.position).isEqualTo(269f / 1280f); assertThat(cue.line).isEqualTo(612f / 720f); assertThat(cue.size).isEqualTo(730f / 1280f); - assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET); + assertThat(cue.bitmapHeight).isEqualTo(43f / 720f); } @Test From ed5ce2396d50672522f0f9888969ad8f2243208b Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 May 2019 19:23:42 +0100 Subject: [PATCH 1326/1556] SimpleCache: Tweak comments related to blocking "Write case, lock not available" was a bit confusing. When the content is not cached and the lock is held, it's neither a read or a write case. It's a "can't do anything" case. When blocking, it may subsequently turn into either a read or a write. PiperOrigin-RevId: 250530722 --- .../exoplayer2/upstream/cache/SimpleCache.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 38c43bd551..1d4481b5cc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -390,10 +390,11 @@ public final class SimpleCache implements Cache { if (span != null) { return span; } else { - // Write case, lock not available. We'll be woken up when a locked span is released (if the - // released lock is for the requested key then we'll be able to make progress) or when a - // span is added to the cache (if the span is for the requested key and covers the requested - // position, then we'll become a read and be able to make progress). + // Lock not available. We'll be woken up when a span is added, or when a locked span is + // released. We'll be able to make progress when either: + // 1. A span is added for the requested key that covers the requested position, in which + // case a read can be started. + // 2. The lock for the requested key is released, in which case a write can be started. wait(); } } @@ -415,12 +416,12 @@ public final class SimpleCache implements Cache { CachedContent cachedContent = contentIndex.getOrAdd(key); if (!cachedContent.isLocked()) { - // Write case, lock available. + // Write case. cachedContent.setLocked(true); return span; } - // Write case, lock not available. + // Lock not available. return null; } From 42ba6abf5a548c05120c586edf2af411f579ca14 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 29 May 2019 20:42:25 +0100 Subject: [PATCH 1327/1556] Fix CacheUtil.cache() use too much data cache() opens all connections with unset length to avoid position errors. This makes more data then needed to be downloading by the underlying network stack. This fix makes makes it open connections for only required length. Issue:#5927 PiperOrigin-RevId: 250546175 --- RELEASENOTES.md | 11 ++-- .../upstream/cache/CacheDataSource.java | 18 +----- .../exoplayer2/upstream/cache/CacheUtil.java | 63 +++++++++++++------ .../exoplayer2/testutil/CacheAsserts.java | 16 +++-- 4 files changed, 62 insertions(+), 46 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3b1ccc3d43..b73d95ba03 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -26,16 +26,19 @@ ([#5784](https://github.com/google/ExoPlayer/issues/5784)). * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). -* Offline: Add Scheduler implementation which uses WorkManager. +* Offline: + * Add Scheduler implementation which uses WorkManager. + * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the + preparation of the `DownloadHelper` failed + ([#5915](https://github.com/google/ExoPlayer/issues/5915)). + * Fix CacheUtil.cache() use too much data + ([#5927](https://github.com/google/ExoPlayer/issues/5927)). * Add a playWhenReady flag to MediaSessionConnector.PlaybackPreparer methods to indicate whether a controller sent a play or only a prepare command. This allows to take advantage of decoder reuse with the MediaSessionConnector ([#5891](https://github.com/google/ExoPlayer/issues/5891)). * Add ProgressUpdateListener to PlayerControlView ([#5834](https://github.com/google/ExoPlayer/issues/5834)). -* Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the - preparation of the `DownloadHelper` failed - ([#5915](https://github.com/google/ExoPlayer/issues/5915)). * Allow enabling decoder fallback with `DefaultRenderersFactory` ([#5942](https://github.com/google/ExoPlayer/issues/5942)). 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 058489f8f0..69bb99451e 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 @@ -136,7 +136,7 @@ public final class CacheDataSource implements DataSource { private boolean currentDataSpecLengthUnset; @Nullable private Uri uri; @Nullable private Uri actualUri; - private @HttpMethod int httpMethod; + @HttpMethod private int httpMethod; private int flags; @Nullable private String key; private long readPosition; @@ -319,7 +319,7 @@ public final class CacheDataSource implements DataSource { } return bytesRead; } catch (IOException e) { - if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) { + if (currentDataSpecLengthUnset && CacheUtil.isCausedByPositionOutOfRange(e)) { setNoBytesRemainingAndMaybeStoreLength(); return C.RESULT_END_OF_INPUT; } @@ -485,20 +485,6 @@ public final class CacheDataSource implements DataSource { return redirectedUri != null ? redirectedUri : defaultUri; } - private static boolean isCausedByPositionOutOfRange(IOException e) { - Throwable cause = e; - while (cause != null) { - if (cause instanceof DataSourceException) { - int reason = ((DataSourceException) cause).reason; - if (reason == DataSourceException.POSITION_OUT_OF_RANGE) { - return true; - } - } - cause = cause.getCause(); - } - return false; - } - private boolean isReadingFromUpstream() { return !isReadingFromCache(); } 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 219d736835..9c80becdeb 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 @@ -20,6 +20,7 @@ import androidx.annotation.Nullable; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.PriorityTaskManager; @@ -195,37 +196,42 @@ public final class CacheUtil { long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key)); bytesLeft = contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - position; } + boolean lengthUnset = bytesLeft == C.LENGTH_UNSET; while (bytesLeft != 0) { throwExceptionIfInterruptedOrCancelled(isCanceled); long blockLength = - cache.getCachedLength( - key, position, bytesLeft != C.LENGTH_UNSET ? bytesLeft : Long.MAX_VALUE); + cache.getCachedLength(key, position, lengthUnset ? Long.MAX_VALUE : bytesLeft); if (blockLength > 0) { // Skip already cached data. } else { // There is a hole in the cache which is at least "-blockLength" long. blockLength = -blockLength; + long length = blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength; + boolean isLastBlock = length == bytesLeft; long read = readAndDiscard( dataSpec, position, - blockLength, + length, dataSource, buffer, priorityTaskManager, priority, progressNotifier, + isLastBlock, isCanceled); if (read < blockLength) { // Reached to the end of the data. - if (enableEOFException && bytesLeft != C.LENGTH_UNSET) { + if (enableEOFException && !lengthUnset) { throw new EOFException(); } break; } } position += blockLength; - bytesLeft -= bytesLeft == C.LENGTH_UNSET ? 0 : blockLength; + if (!lengthUnset) { + bytesLeft -= blockLength; + } } } @@ -242,6 +248,7 @@ public final class CacheUtil { * caching. * @param priority The priority of this task. * @param progressNotifier A notifier through which to report progress updates, or {@code null}. + * @param isLastBlock Whether this read block is the last block of the content. * @param isCanceled An optional flag that will interrupt caching if set to true. * @return Number of read bytes, or 0 if no data is available because the end of the opened range * has been reached. @@ -255,6 +262,7 @@ public final class CacheUtil { PriorityTaskManager priorityTaskManager, int priority, @Nullable ProgressNotifier progressNotifier, + boolean isLastBlock, AtomicBoolean isCanceled) throws IOException, InterruptedException { long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition; @@ -263,22 +271,23 @@ public final class CacheUtil { // Wait for any other thread with higher priority to finish its job. priorityTaskManager.proceed(priority); } + throwExceptionIfInterruptedOrCancelled(isCanceled); try { - throwExceptionIfInterruptedOrCancelled(isCanceled); - // Create a new dataSpec setting length to C.LENGTH_UNSET to prevent getting an error in - // case the given length exceeds the end of input. - dataSpec = - new DataSpec( - dataSpec.uri, - dataSpec.httpMethod, - dataSpec.httpBody, - absoluteStreamPosition, - /* position= */ dataSpec.position + positionOffset, - C.LENGTH_UNSET, - dataSpec.key, - dataSpec.flags); - long resolvedLength = dataSource.open(dataSpec); - if (progressNotifier != null && resolvedLength != C.LENGTH_UNSET) { + long resolvedLength; + try { + resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, length)); + } catch (IOException exception) { + if (length == C.LENGTH_UNSET + || !isLastBlock + || !isCausedByPositionOutOfRange(exception)) { + throw exception; + } + Util.closeQuietly(dataSource); + // Retry to open the data source again, setting length to C.LENGTH_UNSET to prevent + // getting an error in case the given length exceeds the end of input. + resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, C.LENGTH_UNSET)); + } + if (isLastBlock && progressNotifier != null && resolvedLength != C.LENGTH_UNSET) { progressNotifier.onRequestLengthResolved(positionOffset + resolvedLength); } long totalBytesRead = 0; @@ -340,6 +349,20 @@ public final class CacheUtil { } } + /*package*/ static boolean isCausedByPositionOutOfRange(IOException e) { + Throwable cause = e; + while (cause != null) { + if (cause instanceof DataSourceException) { + int reason = ((DataSourceException) cause).reason; + if (reason == DataSourceException.POSITION_OUT_OF_RANGE) { + return true; + } + } + cause = cause.getCause(); + } + return false; + } + private static String buildCacheKey( DataSpec dataSpec, @Nullable CacheKeyFactory cacheKeyFactory) { return (cacheKeyFactory != null ? cacheKeyFactory : DEFAULT_CACHE_KEY_FACTORY) diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java index 9a17904379..a48f88b5c0 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -83,7 +83,8 @@ public final class CacheAsserts { * @throws IOException If an error occurred reading from the Cache. */ public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throws IOException { - DataSpec dataSpec = new DataSpec(uri); + // TODO Make tests specify if the content length is stored in cache metadata. + DataSpec dataSpec = new DataSpec(uri, 0, expected.length, null, 0); assertDataCached(cache, dataSpec, expected); } @@ -95,15 +96,18 @@ public final class CacheAsserts { public static void assertDataCached(Cache cache, DataSpec dataSpec, byte[] expected) throws IOException { DataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0); - dataSource.open(dataSpec); + byte[] bytes; try { - byte[] bytes = TestUtil.readToEnd(dataSource); - assertWithMessage("Cached data doesn't match expected for '" + dataSpec.uri + "',") - .that(bytes) - .isEqualTo(expected); + dataSource.open(dataSpec); + bytes = TestUtil.readToEnd(dataSource); + } catch (IOException e) { + throw new IOException("Opening/reading cache failed: " + dataSpec, e); } finally { dataSource.close(); } + assertWithMessage("Cached data doesn't match expected for '" + dataSpec.uri + "',") + .that(bytes) + .isEqualTo(expected); } /** From e2452f8103a2a45eeeaeab385b2c3005ebad13a8 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 30 May 2019 10:40:00 +0100 Subject: [PATCH 1328/1556] Simplify CacheUtil PiperOrigin-RevId: 250654697 --- .../exoplayer2/upstream/cache/CacheUtil.java | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) 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 9c80becdeb..5b066b7930 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 @@ -79,13 +79,7 @@ public final class CacheUtil { DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) { String key = buildCacheKey(dataSpec, cacheKeyFactory); long position = dataSpec.absoluteStreamPosition; - long requestLength; - if (dataSpec.length != C.LENGTH_UNSET) { - requestLength = dataSpec.length; - } else { - long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key)); - requestLength = contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - position; - } + long requestLength = getRequestLength(dataSpec, cache, key); long bytesAlreadyCached = 0; long bytesLeft = requestLength; while (bytesLeft != 0) { @@ -180,22 +174,19 @@ public final class CacheUtil { Assertions.checkNotNull(dataSource); Assertions.checkNotNull(buffer); + String key = buildCacheKey(dataSpec, cacheKeyFactory); + long bytesLeft; ProgressNotifier progressNotifier = null; if (progressListener != null) { progressNotifier = new ProgressNotifier(progressListener); Pair lengthAndBytesAlreadyCached = getCached(dataSpec, cache, cacheKeyFactory); progressNotifier.init(lengthAndBytesAlreadyCached.first, lengthAndBytesAlreadyCached.second); + bytesLeft = lengthAndBytesAlreadyCached.first; + } else { + bytesLeft = getRequestLength(dataSpec, cache, key); } - String key = buildCacheKey(dataSpec, cacheKeyFactory); long position = dataSpec.absoluteStreamPosition; - long bytesLeft; - if (dataSpec.length != C.LENGTH_UNSET) { - bytesLeft = dataSpec.length; - } else { - long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key)); - bytesLeft = contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - position; - } boolean lengthUnset = bytesLeft == C.LENGTH_UNSET; while (bytesLeft != 0) { throwExceptionIfInterruptedOrCancelled(isCanceled); @@ -235,6 +226,17 @@ public final class CacheUtil { } } + private static long getRequestLength(DataSpec dataSpec, Cache cache, String key) { + if (dataSpec.length != C.LENGTH_UNSET) { + return dataSpec.length; + } else { + long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key)); + return contentLength == C.LENGTH_UNSET + ? C.LENGTH_UNSET + : contentLength - dataSpec.absoluteStreamPosition; + } + } + /** * Reads and discards all data specified by the {@code dataSpec}. * From 00b26a51df6bfcd0422cd4d1747c8f9490e0611f Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 30 May 2019 10:47:28 +0100 Subject: [PATCH 1329/1556] Modify DashDownloaderTest to test if content length is stored PiperOrigin-RevId: 250655481 --- .../dash/offline/DashDownloaderTest.java | 11 +- .../dash/offline/DownloadManagerDashTest.java | 7 +- .../source/hls/offline/HlsDownloaderTest.java | 25 +++-- .../exoplayer2/testutil/CacheAsserts.java | 102 +++++++++++------- 4 files changed, 90 insertions(+), 55 deletions(-) diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java index b3a6b8271b..94dae35ed5 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.offline.Downloader; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderFactory; import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; @@ -108,7 +109,7 @@ public class DashDownloaderTest { DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0)); dashDownloader.download(progressListener); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -127,7 +128,7 @@ public class DashDownloaderTest { DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0)); dashDownloader.download(progressListener); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -146,7 +147,7 @@ public class DashDownloaderTest { DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0)); dashDownloader.download(progressListener); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -167,7 +168,7 @@ public class DashDownloaderTest { DashDownloader dashDownloader = getDashDownloader(fakeDataSet); dashDownloader.download(progressListener); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -256,7 +257,7 @@ public class DashDownloaderTest { // Expected. } dashDownloader.download(progressListener); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index 56fedbefd0..bc75df6acf 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.scheduler.Requirements; +import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable; import com.google.android.exoplayer2.testutil.FakeDataSet; @@ -153,7 +154,7 @@ public class DownloadManagerDashTest { public void testHandleDownloadRequest() throws Throwable { handleDownloadRequest(fakeStreamKey1, fakeStreamKey2); blockUntilTasksCompleteAndThrowAnyDownloadError(); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -161,7 +162,7 @@ public class DownloadManagerDashTest { handleDownloadRequest(fakeStreamKey1); handleDownloadRequest(fakeStreamKey2); blockUntilTasksCompleteAndThrowAnyDownloadError(); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -175,7 +176,7 @@ public class DownloadManagerDashTest { handleDownloadRequest(fakeStreamKey1); blockUntilTasksCompleteAndThrowAnyDownloadError(); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java index 7d77a78316..d06d047f66 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderFactory; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; import com.google.android.exoplayer2.upstream.DummyDataSource; @@ -129,12 +130,13 @@ public class HlsDownloaderTest { assertCachedData( cache, - fakeDataSet, - MASTER_PLAYLIST_URI, - MEDIA_PLAYLIST_1_URI, - MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", - MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", - MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"); + new RequestSet(fakeDataSet) + .subset( + MASTER_PLAYLIST_URI, + MEDIA_PLAYLIST_1_URI, + MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts")); } @Test @@ -186,11 +188,12 @@ public class HlsDownloaderTest { assertCachedData( cache, - fakeDataSet, - MEDIA_PLAYLIST_1_URI, - MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", - MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", - MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"); + new RequestSet(fakeDataSet) + .subset( + MEDIA_PLAYLIST_1_URI, + MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts")); } @Test diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java index a48f88b5c0..4ea4c0844e 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -33,59 +33,89 @@ import java.util.ArrayList; /** Assertion methods for {@link Cache}. */ public final class CacheAsserts { - /** - * Asserts that the cache content is equal to the data in the {@code fakeDataSet}. - * - * @throws IOException If an error occurred reading from the Cache. - */ - public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException { - ArrayList allData = fakeDataSet.getAllData(); - Uri[] uris = new Uri[allData.size()]; - for (int i = 0; i < allData.size(); i++) { - uris[i] = allData.get(i).uri; + /** Defines a set of data requests. */ + public static final class RequestSet { + + private final FakeDataSet fakeDataSet; + private DataSpec[] dataSpecs; + + public RequestSet(FakeDataSet fakeDataSet) { + this.fakeDataSet = fakeDataSet; + ArrayList allData = fakeDataSet.getAllData(); + dataSpecs = new DataSpec[allData.size()]; + for (int i = 0; i < dataSpecs.length; i++) { + dataSpecs[i] = new DataSpec(allData.get(i).uri); + } + } + + public RequestSet subset(String... uriStrings) { + dataSpecs = new DataSpec[uriStrings.length]; + for (int i = 0; i < dataSpecs.length; i++) { + dataSpecs[i] = new DataSpec(Uri.parse(uriStrings[i])); + } + return this; + } + + public RequestSet subset(Uri... uris) { + dataSpecs = new DataSpec[uris.length]; + for (int i = 0; i < dataSpecs.length; i++) { + dataSpecs[i] = new DataSpec(uris[i]); + } + return this; + } + + public RequestSet subset(DataSpec... dataSpecs) { + this.dataSpecs = dataSpecs; + return this; + } + + public int getCount() { + return dataSpecs.length; + } + + public byte[] getData(int i) { + return fakeDataSet.getData(dataSpecs[i].uri).getData(); + } + + public DataSpec getDataSpec(int i) { + return dataSpecs[i]; + } + + public RequestSet useBoundedDataSpecFor(String uriString) { + FakeData data = fakeDataSet.getData(uriString); + for (int i = 0; i < dataSpecs.length; i++) { + DataSpec spec = dataSpecs[i]; + if (spec.uri.getPath().equals(uriString)) { + dataSpecs[i] = spec.subrange(0, data.getData().length); + return this; + } + } + throw new IllegalStateException(); } - assertCachedData(cache, fakeDataSet, uris); } /** - * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. + * Asserts that the cache contains necessary data for the {@code requestSet}. * * @throws IOException If an error occurred reading from the Cache. */ - public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, String... uriStrings) - throws IOException { - Uri[] uris = new Uri[uriStrings.length]; - for (int i = 0; i < uriStrings.length; i++) { - uris[i] = Uri.parse(uriStrings[i]); - } - assertCachedData(cache, fakeDataSet, uris); - } - - /** - * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. - * - * @throws IOException If an error occurred reading from the Cache. - */ - public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, Uri... uris) - throws IOException { + public static void assertCachedData(Cache cache, RequestSet requestSet) throws IOException { int totalLength = 0; - for (Uri uri : uris) { - byte[] data = fakeDataSet.getData(uri).getData(); - assertDataCached(cache, uri, data); + for (int i = 0; i < requestSet.getCount(); i++) { + byte[] data = requestSet.getData(i); + assertDataCached(cache, requestSet.getDataSpec(i), data); totalLength += data.length; } assertThat(cache.getCacheSpace()).isEqualTo(totalLength); } /** - * Asserts that the cache contains the given data for {@code uriString}. + * Asserts that the cache content is equal to the data in the {@code fakeDataSet}. * * @throws IOException If an error occurred reading from the Cache. */ - public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throws IOException { - // TODO Make tests specify if the content length is stored in cache metadata. - DataSpec dataSpec = new DataSpec(uri, 0, expected.length, null, 0); - assertDataCached(cache, dataSpec, expected); + public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException { + assertCachedData(cache, new RequestSet(fakeDataSet)); } /** From b8ec05aea19be5eb3ef317a7daf10ed1d36154b4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 May 2019 11:42:20 +0100 Subject: [PATCH 1330/1556] Handle gzip in DefaultHttpDataSource. Setting the requested encoding in all cases ensures we receive the relevant response headers indicating whether gzip was used. Doing that allows to detect the content length in cases where gzip was requested, but the server replied with uncompressed content. PiperOrigin-RevId: 250660890 --- .../ext/cronet/CronetDataSource.java | 6 +++--- .../upstream/DefaultHttpDataSource.java | 20 ++++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index dd10e5bb66..a1ee80767d 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -466,7 +466,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; // Calculate the content length. - if (!getIsCompressed(responseInfo)) { + if (!isCompressed(responseInfo)) { if (dataSpec.length != C.LENGTH_UNSET) { bytesRemaining = dataSpec.length; } else { @@ -626,7 +626,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } requestBuilder.addHeader("Range", rangeValue.toString()); } - // TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=767025 is fixed + // TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=711810 is fixed // (adjusting the code as necessary). // Force identity encoding unless gzip is allowed. // if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) { @@ -655,7 +655,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs; } - private static boolean getIsCompressed(UrlResponseInfo info) { + private static boolean isCompressed(UrlResponseInfo info) { for (Map.Entry entry : info.getAllHeadersAsList()) { if (entry.getKey().equalsIgnoreCase("Content-Encoding")) { return !entry.getValue().equalsIgnoreCase("identity"); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index 0d16a3f20e..3ee1ef7564 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -41,6 +41,7 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.zip.GZIPInputStream; /** * An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}. @@ -305,7 +306,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; // Determine the length of the data to be read, after skipping. - if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) { + boolean isCompressed = isCompressed(connection); + if (!isCompressed) { if (dataSpec.length != C.LENGTH_UNSET) { bytesToRead = dataSpec.length; } else { @@ -315,14 +317,16 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } } else { // Gzip is enabled. If the server opts to use gzip then the content length in the response - // will be that of the compressed data, which isn't what we want. Furthermore, there isn't a - // reliable way to determine whether the gzip was used or not. Always use the dataSpec length - // in this case. + // will be that of the compressed data, which isn't what we want. Always use the dataSpec + // length in this case. bytesToRead = dataSpec.length; } try { inputStream = connection.getInputStream(); + if (isCompressed) { + inputStream = new GZIPInputStream(inputStream); + } } catch (IOException e) { closeConnectionQuietly(); throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN); @@ -516,9 +520,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou connection.setRequestProperty("Range", rangeRequest); } connection.setRequestProperty("User-Agent", userAgent); - if (!allowGzip) { - connection.setRequestProperty("Accept-Encoding", "identity"); - } + connection.setRequestProperty("Accept-Encoding", allowGzip ? "gzip" : "identity"); if (allowIcyMetadata) { connection.setRequestProperty( IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME, @@ -747,4 +749,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } } + private static boolean isCompressed(HttpURLConnection connection) { + String contentEncoding = connection.getHeaderField("Content-Encoding"); + return "gzip".equalsIgnoreCase(contentEncoding); + } } From 6e7012413bf0f6ae7f3885caa1001988cc5d5f89 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 May 2019 11:54:15 +0100 Subject: [PATCH 1331/1556] Keep controller visible on d-pad key events PiperOrigin-RevId: 250661977 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/ui/PlayerView.java | 19 ++++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b73d95ba03..f98e3dfe84 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -24,6 +24,8 @@ `PlayerControlView`. * Change playback controls toggle from touch down to touch up events ([#5784](https://github.com/google/ExoPlayer/issues/5784)). + * Fix issue where playback controls were not kept visible on key presses + ([#5963](https://github.com/google/ExoPlayer/issues/5963)). * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). * Offline: 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 3575969a78..7e01801daf 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 @@ -772,11 +772,20 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider if (player != null && player.isPlayingAd()) { return super.dispatchKeyEvent(event); } - boolean isDpadWhenControlHidden = - isDpadKey(event.getKeyCode()) && useController && !controller.isVisible(); - boolean handled = - isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); - if (handled) { + + boolean isDpadAndUseController = isDpadKey(event.getKeyCode()) && useController; + boolean handled = false; + if (isDpadAndUseController && !controller.isVisible()) { + // Handle the key event by showing the controller. + maybeShowController(true); + handled = true; + } else if (dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event)) { + // The key event was handled as a media key or by the super class. We should also show the + // controller, or extend its show timeout if already visible. + maybeShowController(true); + handled = true; + } else if (isDpadAndUseController) { + // The key event wasn't handled, but we should extend the controller's show timeout. maybeShowController(true); } return handled; From c09a6eb8eed94bebd224d7eb68062eced1e688e2 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 May 2019 12:19:33 +0100 Subject: [PATCH 1332/1556] Update cast extension build PiperOrigin-RevId: 250664791 --- extensions/cast/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 4dc463ff81..e067789bc4 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -31,7 +31,7 @@ android { } dependencies { - api 'com.google.android.gms:play-services-cast-framework:16.1.2' + api 'com.google.android.gms:play-services-cast-framework:16.2.0' implementation 'androidx.annotation:annotation:1.0.2' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') From 77595da1592836bd988d3a6f9bba473701e30456 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 May 2019 13:01:33 +0100 Subject: [PATCH 1333/1556] Add initial PlaybackStats listener version. This version includes all playback state related metrics and the general listener set-up. PiperOrigin-RevId: 250668729 --- .../exoplayer2/analytics/PlaybackStats.java | 567 ++++++++++++++++ .../analytics/PlaybackStatsListener.java | 614 ++++++++++++++++++ 2 files changed, 1181 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java new file mode 100644 index 0000000000..c30d2ac854 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java @@ -0,0 +1,567 @@ +/* + * Copyright (C) 2019 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.analytics; + +import android.os.SystemClock; +import androidx.annotation.IntDef; +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Collections; +import java.util.List; + +/** Statistics about playbacks. */ +public final class PlaybackStats { + + /** + * State of a playback. One of {@link #PLAYBACK_STATE_NOT_STARTED}, {@link + * #PLAYBACK_STATE_JOINING_FOREGROUND}, {@link #PLAYBACK_STATE_JOINING_BACKGROUND}, {@link + * #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED}, {@link #PLAYBACK_STATE_SEEKING}, + * {@link #PLAYBACK_STATE_BUFFERING}, {@link #PLAYBACK_STATE_PAUSED_BUFFERING}, {@link + * #PLAYBACK_STATE_SEEK_BUFFERING}, {@link #PLAYBACK_STATE_ENDED}, {@link + * #PLAYBACK_STATE_STOPPED}, {@link #PLAYBACK_STATE_FAILED} or {@link #PLAYBACK_STATE_SUSPENDED}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) + @IntDef({ + PLAYBACK_STATE_NOT_STARTED, + PLAYBACK_STATE_JOINING_BACKGROUND, + PLAYBACK_STATE_JOINING_FOREGROUND, + PLAYBACK_STATE_PLAYING, + PLAYBACK_STATE_PAUSED, + PLAYBACK_STATE_SEEKING, + PLAYBACK_STATE_BUFFERING, + PLAYBACK_STATE_PAUSED_BUFFERING, + PLAYBACK_STATE_SEEK_BUFFERING, + PLAYBACK_STATE_ENDED, + PLAYBACK_STATE_STOPPED, + PLAYBACK_STATE_FAILED, + PLAYBACK_STATE_SUSPENDED + }) + @interface PlaybackState {} + /** Playback has not started (initial state). */ + public static final int PLAYBACK_STATE_NOT_STARTED = 0; + /** Playback is buffering in the background for initial playback start. */ + public static final int PLAYBACK_STATE_JOINING_BACKGROUND = 1; + /** Playback is buffering in the foreground for initial playback start. */ + public static final int PLAYBACK_STATE_JOINING_FOREGROUND = 2; + /** Playback is actively playing. */ + public static final int PLAYBACK_STATE_PLAYING = 3; + /** Playback is paused but ready to play. */ + public static final int PLAYBACK_STATE_PAUSED = 4; + /** Playback is handling a seek. */ + public static final int PLAYBACK_STATE_SEEKING = 5; + /** Playback is buffering to restart playback. */ + public static final int PLAYBACK_STATE_BUFFERING = 6; + /** Playback is buffering while paused. */ + public static final int PLAYBACK_STATE_PAUSED_BUFFERING = 7; + /** Playback is buffering after a seek. */ + public static final int PLAYBACK_STATE_SEEK_BUFFERING = 8; + /** Playback has reached the end of the media. */ + public static final int PLAYBACK_STATE_ENDED = 9; + /** Playback is stopped and can be resumed. */ + public static final int PLAYBACK_STATE_STOPPED = 10; + /** Playback is stopped due a fatal error and can be retried. */ + public static final int PLAYBACK_STATE_FAILED = 11; + /** Playback is suspended, e.g. because the user left or it is interrupted by another playback. */ + public static final int PLAYBACK_STATE_SUSPENDED = 12; + /** Total number of playback states. */ + /* package */ static final int PLAYBACK_STATE_COUNT = 13; + + /** Empty playback stats. */ + public static final PlaybackStats EMPTY = merge(/* nothing */ ); + + /** + * Returns the combined {@link PlaybackStats} for all input {@link PlaybackStats}. + * + *

    Note that the full history of events is not kept as the history only makes sense in the + * context of a single playback. + * + * @param playbackStats Array of {@link PlaybackStats} to combine. + * @return The combined {@link PlaybackStats}. + */ + public static PlaybackStats merge(PlaybackStats... playbackStats) { + int playbackCount = 0; + long[] playbackStateDurationsMs = new long[PLAYBACK_STATE_COUNT]; + long firstReportedTimeMs = C.TIME_UNSET; + int foregroundPlaybackCount = 0; + int abandonedBeforeReadyCount = 0; + int endedCount = 0; + int backgroundJoiningCount = 0; + long totalValidJoinTimeMs = C.TIME_UNSET; + int validJoinTimeCount = 0; + int pauseCount = 0; + int pauseBufferCount = 0; + int seekCount = 0; + int rebufferCount = 0; + long maxRebufferTimeMs = C.TIME_UNSET; + int adCount = 0; + for (PlaybackStats stats : playbackStats) { + playbackCount += stats.playbackCount; + for (int i = 0; i < PLAYBACK_STATE_COUNT; i++) { + playbackStateDurationsMs[i] += stats.playbackStateDurationsMs[i]; + } + if (firstReportedTimeMs == C.TIME_UNSET) { + firstReportedTimeMs = stats.firstReportedTimeMs; + } else if (stats.firstReportedTimeMs != C.TIME_UNSET) { + firstReportedTimeMs = Math.min(firstReportedTimeMs, stats.firstReportedTimeMs); + } + foregroundPlaybackCount += stats.foregroundPlaybackCount; + abandonedBeforeReadyCount += stats.abandonedBeforeReadyCount; + endedCount += stats.endedCount; + backgroundJoiningCount += stats.backgroundJoiningCount; + if (totalValidJoinTimeMs == C.TIME_UNSET) { + totalValidJoinTimeMs = stats.totalValidJoinTimeMs; + } else if (stats.totalValidJoinTimeMs != C.TIME_UNSET) { + totalValidJoinTimeMs += stats.totalValidJoinTimeMs; + } + validJoinTimeCount += stats.validJoinTimeCount; + pauseCount += stats.totalPauseCount; + pauseBufferCount += stats.totalPauseBufferCount; + seekCount += stats.totalSeekCount; + rebufferCount += stats.totalRebufferCount; + if (maxRebufferTimeMs == C.TIME_UNSET) { + maxRebufferTimeMs = stats.maxRebufferTimeMs; + } else if (stats.maxRebufferTimeMs != C.TIME_UNSET) { + maxRebufferTimeMs = Math.max(maxRebufferTimeMs, stats.maxRebufferTimeMs); + } + adCount += stats.adPlaybackCount; + } + return new PlaybackStats( + playbackCount, + playbackStateDurationsMs, + /* playbackStateHistory */ Collections.emptyList(), + firstReportedTimeMs, + foregroundPlaybackCount, + abandonedBeforeReadyCount, + endedCount, + backgroundJoiningCount, + totalValidJoinTimeMs, + validJoinTimeCount, + pauseCount, + pauseBufferCount, + seekCount, + rebufferCount, + maxRebufferTimeMs, + adCount); + } + + /** The number of individual playbacks for which these stats were collected. */ + public final int playbackCount; + + // Playback state stats. + + /** + * The playback state history as ordered pairs of the {@link EventTime} at which a state became + * active and the {@link PlaybackState}. + */ + public final List> playbackStateHistory; + /** + * The elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} of the first + * reported playback event, or {@link C#TIME_UNSET} if no event has been reported. + */ + public final long firstReportedTimeMs; + /** The number of playbacks which were the active foreground playback at some point. */ + public final int foregroundPlaybackCount; + /** The number of playbacks which were abandoned before they were ready to play. */ + public final int abandonedBeforeReadyCount; + /** The number of playbacks which reached the ended state at least once. */ + public final int endedCount; + /** The number of playbacks which were pre-buffered in the background. */ + public final int backgroundJoiningCount; + /** + * The total time spent joining the playback, in milliseconds, or {@link C#TIME_UNSET} if no valid + * join time could be determined. + * + *

    Note that this does not include background joining time. A join time may be invalid if the + * playback never reached {@link #PLAYBACK_STATE_PLAYING} or {@link #PLAYBACK_STATE_PAUSED}, or + * joining was interrupted by a seek, stop, or error state. + */ + public final long totalValidJoinTimeMs; + /** + * The number of playbacks with a valid join time as documented in {@link #totalValidJoinTimeMs}. + */ + public final int validJoinTimeCount; + /** The total number of times a playback has been paused. */ + public final int totalPauseCount; + /** The total number of times a playback has been paused while rebuffering. */ + public final int totalPauseBufferCount; + /** + * The total number of times a seek occurred. This includes seeks happening before playback + * resumed after another seek. + */ + public final int totalSeekCount; + /** + * The total number of times a rebuffer occurred. This excludes initial joining and buffering + * after seek. + */ + public final int totalRebufferCount; + /** + * The maximum time spent during a single rebuffer, in milliseconds, or {@link C#TIME_UNSET} if no + * rebuffer occurred. + */ + public final long maxRebufferTimeMs; + /** The number of ad playbacks. */ + public final int adPlaybackCount; + + private final long[] playbackStateDurationsMs; + + /* package */ PlaybackStats( + int playbackCount, + long[] playbackStateDurationsMs, + List> playbackStateHistory, + long firstReportedTimeMs, + int foregroundPlaybackCount, + int abandonedBeforeReadyCount, + int endedCount, + int backgroundJoiningCount, + long totalValidJoinTimeMs, + int validJoinTimeCount, + int totalPauseCount, + int totalPauseBufferCount, + int totalSeekCount, + int totalRebufferCount, + long maxRebufferTimeMs, + int adPlaybackCount) { + this.playbackCount = playbackCount; + this.playbackStateDurationsMs = playbackStateDurationsMs; + this.playbackStateHistory = Collections.unmodifiableList(playbackStateHistory); + this.firstReportedTimeMs = firstReportedTimeMs; + this.foregroundPlaybackCount = foregroundPlaybackCount; + this.abandonedBeforeReadyCount = abandonedBeforeReadyCount; + this.endedCount = endedCount; + this.backgroundJoiningCount = backgroundJoiningCount; + this.totalValidJoinTimeMs = totalValidJoinTimeMs; + this.validJoinTimeCount = validJoinTimeCount; + this.totalPauseCount = totalPauseCount; + this.totalPauseBufferCount = totalPauseBufferCount; + this.totalSeekCount = totalSeekCount; + this.totalRebufferCount = totalRebufferCount; + this.maxRebufferTimeMs = maxRebufferTimeMs; + this.adPlaybackCount = adPlaybackCount; + } + + /** + * Returns the total time spent in a given {@link PlaybackState}, in milliseconds. + * + * @param playbackState A {@link PlaybackState}. + * @return Total spent in the given playback state, in milliseconds + */ + public long getPlaybackStateDurationMs(@PlaybackState int playbackState) { + return playbackStateDurationsMs[playbackState]; + } + + /** + * Returns the {@link PlaybackState} at the given time. + * + * @param realtimeMs The time as returned by {@link SystemClock#elapsedRealtime()}. + * @return The {@link PlaybackState} at that time, or {@link #PLAYBACK_STATE_NOT_STARTED} if the + * given time is before the first known playback state in the history. + */ + @PlaybackState + public int getPlaybackStateAtTime(long realtimeMs) { + @PlaybackState int state = PLAYBACK_STATE_NOT_STARTED; + for (Pair timeAndState : playbackStateHistory) { + if (timeAndState.first.realtimeMs > realtimeMs) { + break; + } + state = timeAndState.second; + } + return state; + } + + /** + * Returns the mean time spent joining the playback, in milliseconds, or {@link C#TIME_UNSET} if + * no valid join time is available. Only includes playbacks with valid join times as documented in + * {@link #totalValidJoinTimeMs}. + */ + public long getMeanJoinTimeMs() { + return validJoinTimeCount == 0 ? C.TIME_UNSET : totalValidJoinTimeMs / validJoinTimeCount; + } + + /** + * Returns the total time spent joining the playback in foreground, in milliseconds. This does + * include invalid join times where the playback never reached {@link #PLAYBACK_STATE_PLAYING} or + * {@link #PLAYBACK_STATE_PAUSED}, or joining was interrupted by a seek, stop, or error state. + */ + public long getTotalJoinTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_JOINING_FOREGROUND); + } + + /** Returns the total time spent actively playing, in milliseconds. */ + public long getTotalPlayTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_PLAYING); + } + + /** + * Returns the mean time spent actively playing per foreground playback, in milliseconds, or + * {@link C#TIME_UNSET} if no playback has been in foreground. + */ + public long getMeanPlayTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalPlayTimeMs() / foregroundPlaybackCount; + } + + /** Returns the total time spent in a paused state, in milliseconds. */ + public long getTotalPausedTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED) + + getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED_BUFFERING); + } + + /** + * Returns the mean time spent in a paused state per foreground playback, in milliseconds, or + * {@link C#TIME_UNSET} if no playback has been in foreground. + */ + public long getMeanPausedTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalPausedTimeMs() / foregroundPlaybackCount; + } + + /** + * Returns the total time spent rebuffering, in milliseconds. This excludes initial join times, + * buffer times after a seek and buffering while paused. + */ + public long getTotalRebufferTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING); + } + + /** + * Returns the mean time spent rebuffering per foreground playback, in milliseconds, or {@link + * C#TIME_UNSET} if no playback has been in foreground. This excludes initial join times, buffer + * times after a seek and buffering while paused. + */ + public long getMeanRebufferTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalRebufferTimeMs() / foregroundPlaybackCount; + } + + /** + * Returns the mean time spent during a single rebuffer, in milliseconds, or {@link C#TIME_UNSET} + * if no rebuffer was recorded. This excludes initial join times and buffer times after a seek. + */ + public long getMeanSingleRebufferTimeMs() { + return totalRebufferCount == 0 + ? C.TIME_UNSET + : (getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING) + + getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED_BUFFERING)) + / totalRebufferCount; + } + + /** + * Returns the total time spent from the start of a seek until playback is ready again, in + * milliseconds. + */ + public long getTotalSeekTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING) + + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING); + } + + /** + * Returns the mean time spent per foreground playback from the start of a seek until playback is + * ready again, in milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground. + */ + public long getMeanSeekTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalSeekTimeMs() / foregroundPlaybackCount; + } + + /** + * Returns the mean time spent from the start of a single seek until playback is ready again, in + * milliseconds, or {@link C#TIME_UNSET} if no seek occurred. + */ + public long getMeanSingleSeekTimeMs() { + return totalSeekCount == 0 ? C.TIME_UNSET : getTotalSeekTimeMs() / totalSeekCount; + } + + /** + * Returns the total time spent actively waiting for playback, in milliseconds. This includes all + * join times, rebuffer times and seek times, but excludes times without user intention to play, + * e.g. all paused states. + */ + public long getTotalWaitTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_JOINING_FOREGROUND) + + getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING) + + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING) + + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING); + } + + /** + * Returns the mean time spent actively waiting for playback per foreground playback, in + * milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground. This includes all + * join times, rebuffer times and seek times, but excludes times without user intention to play, + * e.g. all paused states. + */ + public long getMeanWaitTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalWaitTimeMs() / foregroundPlaybackCount; + } + + /** Returns the total time spent playing or actively waiting for playback, in milliseconds. */ + public long getTotalPlayAndWaitTimeMs() { + return getTotalPlayTimeMs() + getTotalWaitTimeMs(); + } + + /** + * Returns the mean time spent playing or actively waiting for playback per foreground playback, + * in milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground. + */ + public long getMeanPlayAndWaitTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalPlayAndWaitTimeMs() / foregroundPlaybackCount; + } + + /** Returns the total time covered by any playback state, in milliseconds. */ + public long getTotalElapsedTimeMs() { + long totalTimeMs = 0; + for (int i = 0; i < PLAYBACK_STATE_COUNT; i++) { + totalTimeMs += playbackStateDurationsMs[i]; + } + return totalTimeMs; + } + + /** + * Returns the mean time covered by any playback state per playback, in milliseconds, or {@link + * C#TIME_UNSET} if no playback was recorded. + */ + public long getMeanElapsedTimeMs() { + return playbackCount == 0 ? C.TIME_UNSET : getTotalElapsedTimeMs() / playbackCount; + } + + /** + * Returns the ratio of foreground playbacks which were abandoned before they were ready to play, + * or {@code 0.0} if no playback has been in foreground. + */ + public float getAbandonedBeforeReadyRatio() { + int foregroundAbandonedBeforeReady = + abandonedBeforeReadyCount - (playbackCount - foregroundPlaybackCount); + return foregroundPlaybackCount == 0 + ? 0f + : (float) foregroundAbandonedBeforeReady / foregroundPlaybackCount; + } + + /** + * Returns the ratio of foreground playbacks which reached the ended state at least once, or + * {@code 0.0} if no playback has been in foreground. + */ + public float getEndedRatio() { + return foregroundPlaybackCount == 0 ? 0f : (float) endedCount / foregroundPlaybackCount; + } + + /** + * Returns the mean number of times a playback has been paused per foreground playback, or {@code + * 0.0} if no playback has been in foreground. + */ + public float getMeanPauseCount() { + return foregroundPlaybackCount == 0 ? 0f : (float) totalPauseCount / foregroundPlaybackCount; + } + + /** + * Returns the mean number of times a playback has been paused while rebuffering per foreground + * playback, or {@code 0.0} if no playback has been in foreground. + */ + public float getMeanPauseBufferCount() { + return foregroundPlaybackCount == 0 + ? 0f + : (float) totalPauseBufferCount / foregroundPlaybackCount; + } + + /** + * Returns the mean number of times a seek occurred per foreground playback, or {@code 0.0} if no + * playback has been in foreground. This includes seeks happening before playback resumed after + * another seek. + */ + public float getMeanSeekCount() { + return foregroundPlaybackCount == 0 ? 0f : (float) totalSeekCount / foregroundPlaybackCount; + } + + /** + * Returns the mean number of times a rebuffer occurred per foreground playback, or {@code 0.0} if + * no playback has been in foreground. This excludes initial joining and buffering after seek. + */ + public float getMeanRebufferCount() { + return foregroundPlaybackCount == 0 ? 0f : (float) totalRebufferCount / foregroundPlaybackCount; + } + + /** + * Returns the ratio of wait times to the total time spent playing and waiting, or {@code 0.0} if + * no time was spend playing or waiting. This is equivalent to {@link #getTotalWaitTimeMs()} / + * {@link #getTotalPlayAndWaitTimeMs()} and also to {@link #getJoinTimeRatio()} + {@link + * #getRebufferTimeRatio()} + {@link #getSeekTimeRatio()}. + */ + public float getWaitTimeRatio() { + long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs(); + return playAndWaitTimeMs == 0 ? 0f : (float) getTotalWaitTimeMs() / playAndWaitTimeMs; + } + + /** + * Returns the ratio of foreground join time to the total time spent playing and waiting, or + * {@code 0.0} if no time was spend playing or waiting. This is equivalent to {@link + * #getTotalJoinTimeMs()} / {@link #getTotalPlayAndWaitTimeMs()}. + */ + public float getJoinTimeRatio() { + long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs(); + return playAndWaitTimeMs == 0 ? 0f : (float) getTotalJoinTimeMs() / playAndWaitTimeMs; + } + + /** + * Returns the ratio of rebuffer time to the total time spent playing and waiting, or {@code 0.0} + * if no time was spend playing or waiting. This is equivalent to {@link + * #getTotalRebufferTimeMs()} / {@link #getTotalPlayAndWaitTimeMs()}. + */ + public float getRebufferTimeRatio() { + long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs(); + return playAndWaitTimeMs == 0 ? 0f : (float) getTotalRebufferTimeMs() / playAndWaitTimeMs; + } + + /** + * Returns the ratio of seek time to the total time spent playing and waiting, or {@code 0.0} if + * no time was spend playing or waiting. This is equivalent to {@link #getTotalSeekTimeMs()} / + * {@link #getTotalPlayAndWaitTimeMs()}. + */ + public float getSeekTimeRatio() { + long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs(); + return playAndWaitTimeMs == 0 ? 0f : (float) getTotalSeekTimeMs() / playAndWaitTimeMs; + } + + /** + * Returns the rate of rebuffer events, in rebuffers per play time second, or {@code 0.0} if no + * time was spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenRebuffers()}. + */ + public float getRebufferRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * totalRebufferCount / playTimeMs; + } + + /** + * Returns the mean play time between rebuffer events, in seconds. This is equivalent to 1.0 / + * {@link #getRebufferRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}. + */ + public float getMeanTimeBetweenRebuffers() { + return 1f / getRebufferRate(); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java new file mode 100644 index 0000000000..12fc40e817 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -0,0 +1,614 @@ +/* + * Copyright (C) 2019 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.analytics; + +import android.os.SystemClock; +import androidx.annotation.Nullable; +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.analytics.PlaybackStats.PlaybackState; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import com.google.android.exoplayer2.util.Assertions; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * {@link AnalyticsListener} to gather {@link PlaybackStats} from the player. + * + *

    For accurate measurements, the listener should be added to the player before loading media, + * i.e., {@link Player#getPlaybackState()} should be {@link Player#STATE_IDLE}. + * + *

    Playback stats are gathered separately for all playback session, i.e. each window in the + * {@link Timeline} and each single ad. + */ +public final class PlaybackStatsListener + implements AnalyticsListener, PlaybackSessionManager.Listener { + + /** A listener for {@link PlaybackStats} updates. */ + public interface Callback { + + /** + * Called when a playback session ends and its {@link PlaybackStats} are ready. + * + * @param eventTime The {@link EventTime} at which the playback session started. Can be used to + * identify the playback session. + * @param playbackStats The {@link PlaybackStats} for the ended playback session. + */ + void onPlaybackStatsReady(EventTime eventTime, PlaybackStats playbackStats); + } + + private final PlaybackSessionManager sessionManager; + private final Map playbackStatsTrackers; + private final Map sessionStartEventTimes; + @Nullable private final Callback callback; + private final boolean keepHistory; + private final Period period; + + private PlaybackStats finishedPlaybackStats; + @Nullable private String activeContentPlayback; + @Nullable private String activeAdPlayback; + private boolean playWhenReady; + @Player.State private int playbackState; + + /** + * Creates listener for playback stats. + * + * @param keepHistory Whether the reported {@link PlaybackStats} should keep the full history of + * events. + * @param callback An optional callback for finished {@link PlaybackStats}. + */ + public PlaybackStatsListener(boolean keepHistory, @Nullable Callback callback) { + this.callback = callback; + this.keepHistory = keepHistory; + sessionManager = new DefaultPlaybackSessionManager(); + playbackStatsTrackers = new HashMap<>(); + sessionStartEventTimes = new HashMap<>(); + finishedPlaybackStats = PlaybackStats.EMPTY; + playWhenReady = false; + playbackState = Player.STATE_IDLE; + period = new Period(); + sessionManager.setListener(this); + } + + /** + * Returns the combined {@link PlaybackStats} for all playback sessions this listener was and is + * listening to. + * + *

    Note that these {@link PlaybackStats} will not contain the full history of events. + * + * @return The combined {@link PlaybackStats} for all playback sessions. + */ + public PlaybackStats getCombinedPlaybackStats() { + PlaybackStats[] allPendingPlaybackStats = new PlaybackStats[playbackStatsTrackers.size() + 1]; + allPendingPlaybackStats[0] = finishedPlaybackStats; + int index = 1; + for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) { + allPendingPlaybackStats[index++] = tracker.build(/* isFinal= */ false); + } + return PlaybackStats.merge(allPendingPlaybackStats); + } + + /** + * Returns the {@link PlaybackStats} for the currently playback session, or null if no session is + * active. + * + * @return {@link PlaybackStats} for the current playback session. + */ + @Nullable + public PlaybackStats getPlaybackStats() { + PlaybackStatsTracker activeStatsTracker = + activeAdPlayback != null + ? playbackStatsTrackers.get(activeAdPlayback) + : activeContentPlayback != null + ? playbackStatsTrackers.get(activeContentPlayback) + : null; + return activeStatsTracker == null ? null : activeStatsTracker.build(/* isFinal= */ false); + } + + /** + * Finishes all pending playback sessions. Should be called when the listener is removed from the + * player or when the player is released. + */ + public void finishAllSessions() { + // TODO: Add AnalyticsListener.onAttachedToPlayer and onDetachedFromPlayer to auto-release with + // an actual EventTime. Should also simplify other cases where the listener needs to be released + // separately from the player. + HashMap trackerCopy = new HashMap<>(playbackStatsTrackers); + EventTime dummyEventTime = + new EventTime( + SystemClock.elapsedRealtime(), + Timeline.EMPTY, + /* windowIndex= */ 0, + /* mediaPeriodId= */ null, + /* eventPlaybackPositionMs= */ 0, + /* currentPlaybackPositionMs= */ 0, + /* totalBufferedDurationMs= */ 0); + for (String session : trackerCopy.keySet()) { + onSessionFinished(dummyEventTime, session, /* automaticTransition= */ false); + } + } + + // PlaybackSessionManager.Listener implementation. + + @Override + public void onSessionCreated(EventTime eventTime, String session) { + PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime); + tracker.onPlayerStateChanged( + eventTime, playWhenReady, playbackState, /* belongsToPlayback= */ true); + playbackStatsTrackers.put(session, tracker); + sessionStartEventTimes.put(session, eventTime); + } + + @Override + public void onSessionActive(EventTime eventTime, String session) { + Assertions.checkNotNull(playbackStatsTrackers.get(session)).onForeground(eventTime); + if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) { + activeAdPlayback = session; + } else { + activeContentPlayback = session; + } + } + + @Override + public void onAdPlaybackStarted(EventTime eventTime, String contentSession, String adSession) { + Assertions.checkState(Assertions.checkNotNull(eventTime.mediaPeriodId).isAd()); + long contentPositionUs = + eventTime + .timeline + .getPeriodByUid(eventTime.mediaPeriodId.periodUid, period) + .getAdGroupTimeUs(eventTime.mediaPeriodId.adGroupIndex); + EventTime contentEventTime = + new EventTime( + eventTime.realtimeMs, + eventTime.timeline, + eventTime.windowIndex, + new MediaPeriodId( + eventTime.mediaPeriodId.periodUid, + eventTime.mediaPeriodId.windowSequenceNumber, + eventTime.mediaPeriodId.adGroupIndex), + /* eventPlaybackPositionMs= */ C.usToMs(contentPositionUs), + eventTime.currentPlaybackPositionMs, + eventTime.totalBufferedDurationMs); + Assertions.checkNotNull(playbackStatsTrackers.get(contentSession)) + .onSuspended(contentEventTime, /* belongsToPlayback= */ true); + } + + @Override + public void onSessionFinished(EventTime eventTime, String session, boolean automaticTransition) { + if (session.equals(activeAdPlayback)) { + activeAdPlayback = null; + } else if (session.equals(activeContentPlayback)) { + activeContentPlayback = null; + } + PlaybackStatsTracker tracker = Assertions.checkNotNull(playbackStatsTrackers.remove(session)); + EventTime startEventTime = Assertions.checkNotNull(sessionStartEventTimes.remove(session)); + if (automaticTransition) { + // Simulate ENDED state to record natural ending of playback. + tracker.onPlayerStateChanged( + eventTime, /* playWhenReady= */ true, Player.STATE_ENDED, /* belongsToPlayback= */ false); + } + tracker.onSuspended(eventTime, /* belongsToPlayback= */ false); + PlaybackStats playbackStats = tracker.build(/* isFinal= */ true); + finishedPlaybackStats = PlaybackStats.merge(finishedPlaybackStats, playbackStats); + if (callback != null) { + callback.onPlaybackStatsReady(startEventTime, playbackStats); + } + } + + // AnalyticsListener implementation. + + @Override + public void onPlayerStateChanged( + EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) { + this.playWhenReady = playWhenReady; + this.playbackState = playbackState; + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); + playbackStatsTrackers + .get(session) + .onPlayerStateChanged(eventTime, playWhenReady, playbackState, belongsToPlayback); + } + } + + @Override + public void onTimelineChanged(EventTime eventTime, int reason) { + sessionManager.handleTimelineUpdate(eventTime); + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime); + } + } + } + + @Override + public void onPositionDiscontinuity(EventTime eventTime, int reason) { + sessionManager.handlePositionDiscontinuity(eventTime, reason); + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime); + } + } + } + + @Override + public void onSeekStarted(EventTime eventTime) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onSeekStarted(eventTime); + } + } + } + + @Override + public void onSeekProcessed(EventTime eventTime) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onSeekProcessed(eventTime); + } + } + } + + @Override + public void onPlayerError(EventTime eventTime, ExoPlaybackException error) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onFatalError(eventTime, error); + } + } + } + + @Override + public void onLoadStarted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onLoadStarted(eventTime); + } + } + } + + /** Tracker for playback stats of a single playback. */ + private static final class PlaybackStatsTracker { + + // Final stats. + private final boolean keepHistory; + private final long[] playbackStateDurationsMs; + private final List> playbackStateHistory; + private final boolean isAd; + + private long firstReportedTimeMs; + private boolean hasBeenReady; + private boolean hasEnded; + private boolean isJoinTimeInvalid; + private int pauseCount; + private int pauseBufferCount; + private int seekCount; + private int rebufferCount; + private long maxRebufferTimeMs; + + // Current player state tracking. + @PlaybackState private int currentPlaybackState; + private long currentPlaybackStateStartTimeMs; + private boolean isSeeking; + private boolean isForeground; + private boolean isSuspended; + private boolean playWhenReady; + @Player.State private int playerPlaybackState; + private boolean hasFatalError; + private boolean startedLoading; + private long lastRebufferStartTimeMs; + + /** + * Creates a tracker for playback stats. + * + * @param keepHistory Whether to keep a full history of events. + * @param startTime The {@link EventTime} at which the playback stats start. + */ + public PlaybackStatsTracker(boolean keepHistory, EventTime startTime) { + this.keepHistory = keepHistory; + playbackStateDurationsMs = new long[PlaybackStats.PLAYBACK_STATE_COUNT]; + playbackStateHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + currentPlaybackState = PlaybackStats.PLAYBACK_STATE_NOT_STARTED; + currentPlaybackStateStartTimeMs = startTime.realtimeMs; + playerPlaybackState = Player.STATE_IDLE; + firstReportedTimeMs = C.TIME_UNSET; + maxRebufferTimeMs = C.TIME_UNSET; + isAd = startTime.mediaPeriodId != null && startTime.mediaPeriodId.isAd(); + } + + /** + * Notifies the tracker of a player state change event, including all player state changes while + * the playback is not in the foreground. + * + * @param eventTime The {@link EventTime}. + * @param playWhenReady Whether the playback will proceed when ready. + * @param playbackState The current {@link Player.State}. + * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. + */ + public void onPlayerStateChanged( + EventTime eventTime, + boolean playWhenReady, + @Player.State int playbackState, + boolean belongsToPlayback) { + this.playWhenReady = playWhenReady; + playerPlaybackState = playbackState; + if (playbackState != Player.STATE_IDLE) { + hasFatalError = false; + } + if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED) { + isSuspended = false; + } + maybeUpdatePlaybackState(eventTime, belongsToPlayback); + } + + /** + * Notifies the tracker of a position discontinuity or timeline update for the current playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onPositionDiscontinuity(EventTime eventTime) { + isSuspended = false; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker of the start of a seek in the current playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onSeekStarted(EventTime eventTime) { + isSeeking = true; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker of a seek has been processed in the current playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onSeekProcessed(EventTime eventTime) { + isSeeking = false; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker of fatal player error in the current playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onFatalError(EventTime eventTime, Exception error) { + hasFatalError = true; + isSuspended = false; + isSeeking = false; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker that a load for the current playback has started. + * + * @param eventTime The {@link EventTime}. + */ + public void onLoadStarted(EventTime eventTime) { + startedLoading = true; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker that the current playback became the active foreground playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onForeground(EventTime eventTime) { + isForeground = true; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker that the current playback has been suspended, e.g. for ad playback or + * permanently. + * + * @param eventTime The {@link EventTime}. + * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. + */ + public void onSuspended(EventTime eventTime, boolean belongsToPlayback) { + isSuspended = true; + isSeeking = false; + maybeUpdatePlaybackState(eventTime, belongsToPlayback); + } + + /** + * Builds the playback stats. + * + * @param isFinal Whether this is the final build and no further events are expected. + */ + public PlaybackStats build(boolean isFinal) { + long[] playbackStateDurationsMs = this.playbackStateDurationsMs; + if (!isFinal) { + long buildTimeMs = SystemClock.elapsedRealtime(); + playbackStateDurationsMs = + Arrays.copyOf(this.playbackStateDurationsMs, PlaybackStats.PLAYBACK_STATE_COUNT); + long lastStateDurationMs = Math.max(0, buildTimeMs - currentPlaybackStateStartTimeMs); + playbackStateDurationsMs[currentPlaybackState] += lastStateDurationMs; + maybeUpdateMaxRebufferTimeMs(buildTimeMs); + } + boolean isJoinTimeInvalid = this.isJoinTimeInvalid || !hasBeenReady; + long validJoinTimeMs = + isJoinTimeInvalid + ? C.TIME_UNSET + : playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND]; + boolean hasBackgroundJoin = + playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND] > 0; + return new PlaybackStats( + /* playbackCount= */ 1, + playbackStateDurationsMs, + isFinal ? playbackStateHistory : new ArrayList<>(playbackStateHistory), + firstReportedTimeMs, + /* foregroundPlaybackCount= */ isForeground ? 1 : 0, + /* abandonedBeforeReadyCount= */ hasBeenReady ? 0 : 1, + /* endedCount= */ hasEnded ? 1 : 0, + /* backgroundJoiningCount= */ hasBackgroundJoin ? 1 : 0, + validJoinTimeMs, + /* validJoinTimeCount= */ isJoinTimeInvalid ? 0 : 1, + pauseCount, + pauseBufferCount, + seekCount, + rebufferCount, + maxRebufferTimeMs, + /* adPlaybackCount= */ isAd ? 1 : 0); + } + + private void maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlayback) { + @PlaybackState int newPlaybackState = resolveNewPlaybackState(); + if (newPlaybackState == currentPlaybackState) { + return; + } + Assertions.checkArgument(eventTime.realtimeMs >= currentPlaybackStateStartTimeMs); + + long stateDurationMs = eventTime.realtimeMs - currentPlaybackStateStartTimeMs; + playbackStateDurationsMs[currentPlaybackState] += stateDurationMs; + if (firstReportedTimeMs == C.TIME_UNSET) { + firstReportedTimeMs = eventTime.realtimeMs; + } + isJoinTimeInvalid |= isInvalidJoinTransition(currentPlaybackState, newPlaybackState); + hasBeenReady |= isReadyState(newPlaybackState); + hasEnded |= newPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED; + if (!isPausedState(currentPlaybackState) && isPausedState(newPlaybackState)) { + pauseCount++; + } + if (newPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING) { + seekCount++; + } + if (!isRebufferingState(currentPlaybackState) && isRebufferingState(newPlaybackState)) { + rebufferCount++; + lastRebufferStartTimeMs = eventTime.realtimeMs; + } + if (newPlaybackState == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING + && currentPlaybackState == PlaybackStats.PLAYBACK_STATE_BUFFERING) { + pauseBufferCount++; + } + + maybeUpdateMaxRebufferTimeMs(eventTime.realtimeMs); + + currentPlaybackState = newPlaybackState; + currentPlaybackStateStartTimeMs = eventTime.realtimeMs; + if (keepHistory) { + playbackStateHistory.add(Pair.create(eventTime, currentPlaybackState)); + } + } + + @PlaybackState + private int resolveNewPlaybackState() { + if (isSuspended) { + // Keep VIDEO_STATE_ENDED if playback naturally ended (or progressed to next item). + return currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED + ? PlaybackStats.PLAYBACK_STATE_ENDED + : PlaybackStats.PLAYBACK_STATE_SUSPENDED; + } else if (isSeeking) { + // Seeking takes precedence over errors such that we report a seek while in error state. + return PlaybackStats.PLAYBACK_STATE_SEEKING; + } else if (hasFatalError) { + return PlaybackStats.PLAYBACK_STATE_FAILED; + } else if (!isForeground) { + // Before the playback becomes foreground, only report background joining and not started. + return startedLoading + ? PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND + : PlaybackStats.PLAYBACK_STATE_NOT_STARTED; + } else if (playerPlaybackState == Player.STATE_ENDED) { + return PlaybackStats.PLAYBACK_STATE_ENDED; + } else if (playerPlaybackState == Player.STATE_BUFFERING) { + if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_NOT_STARTED + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SUSPENDED) { + return PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND; + } + if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING) { + return PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING; + } + return playWhenReady + ? PlaybackStats.PLAYBACK_STATE_BUFFERING + : PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; + } else if (playerPlaybackState == Player.STATE_READY) { + return playWhenReady + ? PlaybackStats.PLAYBACK_STATE_PLAYING + : PlaybackStats.PLAYBACK_STATE_PAUSED; + } else if (playerPlaybackState == Player.STATE_IDLE + && currentPlaybackState != PlaybackStats.PLAYBACK_STATE_NOT_STARTED) { + // This case only applies for calls to player.stop(). All other IDLE cases are handled by + // !isForeground, hasFatalError or isSuspended. NOT_STARTED is deliberately ignored. + return PlaybackStats.PLAYBACK_STATE_STOPPED; + } + return currentPlaybackState; + } + + private void maybeUpdateMaxRebufferTimeMs(long nowMs) { + if (isRebufferingState(currentPlaybackState)) { + long rebufferDurationMs = nowMs - lastRebufferStartTimeMs; + if (maxRebufferTimeMs == C.TIME_UNSET || rebufferDurationMs > maxRebufferTimeMs) { + maxRebufferTimeMs = rebufferDurationMs; + } + } + } + + private static boolean isReadyState(@PlaybackState int state) { + return state == PlaybackStats.PLAYBACK_STATE_PLAYING + || state == PlaybackStats.PLAYBACK_STATE_PAUSED; + } + + private static boolean isPausedState(@PlaybackState int state) { + return state == PlaybackStats.PLAYBACK_STATE_PAUSED + || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; + } + + private static boolean isRebufferingState(@PlaybackState int state) { + return state == PlaybackStats.PLAYBACK_STATE_BUFFERING + || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; + } + + private static boolean isInvalidJoinTransition( + @PlaybackState int oldState, @PlaybackState int newState) { + if (oldState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND + && oldState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND + && oldState != PlaybackStats.PLAYBACK_STATE_SUSPENDED) { + return false; + } + return newState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND + && newState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND + && newState != PlaybackStats.PLAYBACK_STATE_SUSPENDED + && newState != PlaybackStats.PLAYBACK_STATE_PLAYING + && newState != PlaybackStats.PLAYBACK_STATE_PAUSED + && newState != PlaybackStats.PLAYBACK_STATE_ENDED; + } + } +} From fd1179aaa14b4973ba335fe001ac0224678f6b02 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 May 2019 13:02:01 +0100 Subject: [PATCH 1334/1556] Add remaining PlaybackStatsListener metrics. This adds all the non-playback-state metrics, like format, error, bandwidth and renderer performance metrics. PiperOrigin-RevId: 250668854 --- RELEASENOTES.md | 2 + .../com/google/android/exoplayer2/Format.java | 32 ++ .../exoplayer2/analytics/PlaybackStats.java | 435 +++++++++++++++++- .../analytics/PlaybackStatsListener.java | 390 +++++++++++++++- 4 files changed, 842 insertions(+), 17 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f98e3dfe84..83a323ec62 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* Add `PlaybackStatsListener` to collect `PlaybackStats` for playbacks analysis + and analytics reporting (TODO: link to developer guide page/blog post). * Add basic DRM support to the Cast demo app. * Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s ([#5779](https://github.com/google/ExoPlayer/issues/5779)). 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 cf1c6f4e5a..a482022a17 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 @@ -1373,6 +1373,38 @@ public final class Format implements Parcelable { accessibilityChannel); } + public Format copyWithVideoSize(int width, int height) { + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel); + } + /** * Returns the number of pixels if this is a video format whose {@link #width} and {@link #height} * are known, or {@link #NO_VALUE} otherwise diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java index c30d2ac854..f633bfbf8e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java @@ -19,6 +19,7 @@ import android.os.SystemClock; import androidx.annotation.IntDef; import android.util.Pair; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -27,6 +28,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.Collections; import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** Statistics about playbacks. */ public final class PlaybackStats { @@ -109,12 +111,31 @@ public final class PlaybackStats { int backgroundJoiningCount = 0; long totalValidJoinTimeMs = C.TIME_UNSET; int validJoinTimeCount = 0; - int pauseCount = 0; - int pauseBufferCount = 0; - int seekCount = 0; - int rebufferCount = 0; + int totalPauseCount = 0; + int totalPauseBufferCount = 0; + int totalSeekCount = 0; + int totalRebufferCount = 0; long maxRebufferTimeMs = C.TIME_UNSET; - int adCount = 0; + int adPlaybackCount = 0; + long totalVideoFormatHeightTimeMs = 0; + long totalVideoFormatHeightTimeProduct = 0; + long totalVideoFormatBitrateTimeMs = 0; + long totalVideoFormatBitrateTimeProduct = 0; + long totalAudioFormatTimeMs = 0; + long totalAudioFormatBitrateTimeProduct = 0; + int initialVideoFormatHeightCount = 0; + int initialVideoFormatBitrateCount = 0; + int totalInitialVideoFormatHeight = C.LENGTH_UNSET; + long totalInitialVideoFormatBitrate = C.LENGTH_UNSET; + int initialAudioFormatBitrateCount = 0; + long totalInitialAudioFormatBitrate = C.LENGTH_UNSET; + long totalBandwidthTimeMs = 0; + long totalBandwidthBytes = 0; + long totalDroppedFrames = 0; + long totalAudioUnderruns = 0; + int fatalErrorPlaybackCount = 0; + int fatalErrorCount = 0; + int nonFatalErrorCount = 0; for (PlaybackStats stats : playbackStats) { playbackCount += stats.playbackCount; for (int i = 0; i < PLAYBACK_STATE_COUNT; i++) { @@ -135,21 +156,53 @@ public final class PlaybackStats { totalValidJoinTimeMs += stats.totalValidJoinTimeMs; } validJoinTimeCount += stats.validJoinTimeCount; - pauseCount += stats.totalPauseCount; - pauseBufferCount += stats.totalPauseBufferCount; - seekCount += stats.totalSeekCount; - rebufferCount += stats.totalRebufferCount; + totalPauseCount += stats.totalPauseCount; + totalPauseBufferCount += stats.totalPauseBufferCount; + totalSeekCount += stats.totalSeekCount; + totalRebufferCount += stats.totalRebufferCount; if (maxRebufferTimeMs == C.TIME_UNSET) { maxRebufferTimeMs = stats.maxRebufferTimeMs; } else if (stats.maxRebufferTimeMs != C.TIME_UNSET) { maxRebufferTimeMs = Math.max(maxRebufferTimeMs, stats.maxRebufferTimeMs); } - adCount += stats.adPlaybackCount; + adPlaybackCount += stats.adPlaybackCount; + totalVideoFormatHeightTimeMs += stats.totalVideoFormatHeightTimeMs; + totalVideoFormatHeightTimeProduct += stats.totalVideoFormatHeightTimeProduct; + totalVideoFormatBitrateTimeMs += stats.totalVideoFormatBitrateTimeMs; + totalVideoFormatBitrateTimeProduct += stats.totalVideoFormatBitrateTimeProduct; + totalAudioFormatTimeMs += stats.totalAudioFormatTimeMs; + totalAudioFormatBitrateTimeProduct += stats.totalAudioFormatBitrateTimeProduct; + initialVideoFormatHeightCount += stats.initialVideoFormatHeightCount; + initialVideoFormatBitrateCount += stats.initialVideoFormatBitrateCount; + if (totalInitialVideoFormatHeight == C.LENGTH_UNSET) { + totalInitialVideoFormatHeight = stats.totalInitialVideoFormatHeight; + } else if (stats.totalInitialVideoFormatHeight != C.LENGTH_UNSET) { + totalInitialVideoFormatHeight += stats.totalInitialVideoFormatHeight; + } + if (totalInitialVideoFormatBitrate == C.LENGTH_UNSET) { + totalInitialVideoFormatBitrate = stats.totalInitialVideoFormatBitrate; + } else if (stats.totalInitialVideoFormatBitrate != C.LENGTH_UNSET) { + totalInitialVideoFormatBitrate += stats.totalInitialVideoFormatBitrate; + } + initialAudioFormatBitrateCount += stats.initialAudioFormatBitrateCount; + if (totalInitialAudioFormatBitrate == C.LENGTH_UNSET) { + totalInitialAudioFormatBitrate = stats.totalInitialAudioFormatBitrate; + } else if (stats.totalInitialAudioFormatBitrate != C.LENGTH_UNSET) { + totalInitialAudioFormatBitrate += stats.totalInitialAudioFormatBitrate; + } + totalBandwidthTimeMs += stats.totalBandwidthTimeMs; + totalBandwidthBytes += stats.totalBandwidthBytes; + totalDroppedFrames += stats.totalDroppedFrames; + totalAudioUnderruns += stats.totalAudioUnderruns; + fatalErrorPlaybackCount += stats.fatalErrorPlaybackCount; + fatalErrorCount += stats.fatalErrorCount; + nonFatalErrorCount += stats.nonFatalErrorCount; } return new PlaybackStats( playbackCount, playbackStateDurationsMs, /* playbackStateHistory */ Collections.emptyList(), + /* mediaTimeHistory= */ Collections.emptyList(), firstReportedTimeMs, foregroundPlaybackCount, abandonedBeforeReadyCount, @@ -157,12 +210,35 @@ public final class PlaybackStats { backgroundJoiningCount, totalValidJoinTimeMs, validJoinTimeCount, - pauseCount, - pauseBufferCount, - seekCount, - rebufferCount, + totalPauseCount, + totalPauseBufferCount, + totalSeekCount, + totalRebufferCount, maxRebufferTimeMs, - adCount); + adPlaybackCount, + /* videoFormatHistory= */ Collections.emptyList(), + /* audioFormatHistory= */ Collections.emptyList(), + totalVideoFormatHeightTimeMs, + totalVideoFormatHeightTimeProduct, + totalVideoFormatBitrateTimeMs, + totalVideoFormatBitrateTimeProduct, + totalAudioFormatTimeMs, + totalAudioFormatBitrateTimeProduct, + initialVideoFormatHeightCount, + initialVideoFormatBitrateCount, + totalInitialVideoFormatHeight, + totalInitialVideoFormatBitrate, + initialAudioFormatBitrateCount, + totalInitialAudioFormatBitrate, + totalBandwidthTimeMs, + totalBandwidthBytes, + totalDroppedFrames, + totalAudioUnderruns, + fatalErrorPlaybackCount, + fatalErrorCount, + nonFatalErrorCount, + /* fatalErrorHistory= */ Collections.emptyList(), + /* nonFatalErrorHistory= */ Collections.emptyList()); } /** The number of individual playbacks for which these stats were collected. */ @@ -175,6 +251,12 @@ public final class PlaybackStats { * active and the {@link PlaybackState}. */ public final List> playbackStateHistory; + /** + * The media time history as an ordered list of long[2] arrays with [0] being the realtime as + * returned by {@code SystemClock.elapsedRealtime()} and [1] being the media time at this + * realtime, in milliseconds. + */ + public final List mediaTimeHistory; /** * The elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} of the first * reported playback event, or {@link C#TIME_UNSET} if no event has been reported. @@ -223,12 +305,108 @@ public final class PlaybackStats { /** The number of ad playbacks. */ public final int adPlaybackCount; + // Format stats. + + /** + * The video format history as ordered pairs of the {@link EventTime} at which a format started + * being used and the {@link Format}. The {@link Format} may be null if no video format was used. + */ + public final List> videoFormatHistory; + /** + * The audio format history as ordered pairs of the {@link EventTime} at which a format started + * being used and the {@link Format}. The {@link Format} may be null if no audio format was used. + */ + public final List> audioFormatHistory; + /** The total media time for which video format height data is available, in milliseconds. */ + public final long totalVideoFormatHeightTimeMs; + /** + * The accumulated sum of all video format heights, in pixels, times the time the format was used + * for playback, in milliseconds. + */ + public final long totalVideoFormatHeightTimeProduct; + /** The total media time for which video format bitrate data is available, in milliseconds. */ + public final long totalVideoFormatBitrateTimeMs; + /** + * The accumulated sum of all video format bitrates, in bits per second, times the time the format + * was used for playback, in milliseconds. + */ + public final long totalVideoFormatBitrateTimeProduct; + /** The total media time for which audio format data is available, in milliseconds. */ + public final long totalAudioFormatTimeMs; + /** + * The accumulated sum of all audio format bitrates, in bits per second, times the time the format + * was used for playback, in milliseconds. + */ + public final long totalAudioFormatBitrateTimeProduct; + /** The number of playbacks with initial video format height data. */ + public final int initialVideoFormatHeightCount; + /** The number of playbacks with initial video format bitrate data. */ + public final int initialVideoFormatBitrateCount; + /** + * The total initial video format height for all playbacks, in pixels, or {@link C#LENGTH_UNSET} + * if no initial video format data is available. + */ + public final int totalInitialVideoFormatHeight; + /** + * The total initial video format bitrate for all playbacks, in bits per second, or {@link + * C#LENGTH_UNSET} if no initial video format data is available. + */ + public final long totalInitialVideoFormatBitrate; + /** The number of playbacks with initial audio format bitrate data. */ + public final int initialAudioFormatBitrateCount; + /** + * The total initial audio format bitrate for all playbacks, in bits per second, or {@link + * C#LENGTH_UNSET} if no initial audio format data is available. + */ + public final long totalInitialAudioFormatBitrate; + + // Bandwidth stats. + + /** The total time for which bandwidth measurement data is available, in milliseconds. */ + public final long totalBandwidthTimeMs; + /** The total bytes transferred during {@link #totalBandwidthTimeMs}. */ + public final long totalBandwidthBytes; + + // Renderer quality stats. + + /** The total number of dropped video frames. */ + public final long totalDroppedFrames; + /** The total number of audio underruns. */ + public final long totalAudioUnderruns; + + // Error stats. + + /** + * The total number of playback with at least one fatal error. Errors are fatal if playback + * stopped due to this error. + */ + public final int fatalErrorPlaybackCount; + /** The total number of fatal errors. Errors are fatal if playback stopped due to this error. */ + public final int fatalErrorCount; + /** + * The total number of non-fatal errors. Error are non-fatal if playback can recover from the + * error without stopping. + */ + public final int nonFatalErrorCount; + /** + * The history of fatal errors as ordered pairs of the {@link EventTime} at which an error + * occurred and the error. Errors are fatal if playback stopped due to this error. + */ + public final List> fatalErrorHistory; + /** + * The history of non-fatal errors as ordered pairs of the {@link EventTime} at which an error + * occurred and the error. Error are non-fatal if playback can recover from the error without + * stopping. + */ + public final List> nonFatalErrorHistory; + private final long[] playbackStateDurationsMs; /* package */ PlaybackStats( int playbackCount, long[] playbackStateDurationsMs, List> playbackStateHistory, + List mediaTimeHistory, long firstReportedTimeMs, int foregroundPlaybackCount, int abandonedBeforeReadyCount, @@ -241,10 +419,34 @@ public final class PlaybackStats { int totalSeekCount, int totalRebufferCount, long maxRebufferTimeMs, - int adPlaybackCount) { + int adPlaybackCount, + List> videoFormatHistory, + List> audioFormatHistory, + long totalVideoFormatHeightTimeMs, + long totalVideoFormatHeightTimeProduct, + long totalVideoFormatBitrateTimeMs, + long totalVideoFormatBitrateTimeProduct, + long totalAudioFormatTimeMs, + long totalAudioFormatBitrateTimeProduct, + int initialVideoFormatHeightCount, + int initialVideoFormatBitrateCount, + int totalInitialVideoFormatHeight, + long totalInitialVideoFormatBitrate, + int initialAudioFormatBitrateCount, + long totalInitialAudioFormatBitrate, + long totalBandwidthTimeMs, + long totalBandwidthBytes, + long totalDroppedFrames, + long totalAudioUnderruns, + int fatalErrorPlaybackCount, + int fatalErrorCount, + int nonFatalErrorCount, + List> fatalErrorHistory, + List> nonFatalErrorHistory) { this.playbackCount = playbackCount; this.playbackStateDurationsMs = playbackStateDurationsMs; this.playbackStateHistory = Collections.unmodifiableList(playbackStateHistory); + this.mediaTimeHistory = Collections.unmodifiableList(mediaTimeHistory); this.firstReportedTimeMs = firstReportedTimeMs; this.foregroundPlaybackCount = foregroundPlaybackCount; this.abandonedBeforeReadyCount = abandonedBeforeReadyCount; @@ -258,6 +460,29 @@ public final class PlaybackStats { this.totalRebufferCount = totalRebufferCount; this.maxRebufferTimeMs = maxRebufferTimeMs; this.adPlaybackCount = adPlaybackCount; + this.videoFormatHistory = Collections.unmodifiableList(videoFormatHistory); + this.audioFormatHistory = Collections.unmodifiableList(audioFormatHistory); + this.totalVideoFormatHeightTimeMs = totalVideoFormatHeightTimeMs; + this.totalVideoFormatHeightTimeProduct = totalVideoFormatHeightTimeProduct; + this.totalVideoFormatBitrateTimeMs = totalVideoFormatBitrateTimeMs; + this.totalVideoFormatBitrateTimeProduct = totalVideoFormatBitrateTimeProduct; + this.totalAudioFormatTimeMs = totalAudioFormatTimeMs; + this.totalAudioFormatBitrateTimeProduct = totalAudioFormatBitrateTimeProduct; + this.initialVideoFormatHeightCount = initialVideoFormatHeightCount; + this.initialVideoFormatBitrateCount = initialVideoFormatBitrateCount; + this.totalInitialVideoFormatHeight = totalInitialVideoFormatHeight; + this.totalInitialVideoFormatBitrate = totalInitialVideoFormatBitrate; + this.initialAudioFormatBitrateCount = initialAudioFormatBitrateCount; + this.totalInitialAudioFormatBitrate = totalInitialAudioFormatBitrate; + this.totalBandwidthTimeMs = totalBandwidthTimeMs; + this.totalBandwidthBytes = totalBandwidthBytes; + this.totalDroppedFrames = totalDroppedFrames; + this.totalAudioUnderruns = totalAudioUnderruns; + this.fatalErrorPlaybackCount = fatalErrorPlaybackCount; + this.fatalErrorCount = fatalErrorCount; + this.nonFatalErrorCount = nonFatalErrorCount; + this.fatalErrorHistory = Collections.unmodifiableList(fatalErrorHistory); + this.nonFatalErrorHistory = Collections.unmodifiableList(nonFatalErrorHistory); } /** @@ -289,6 +514,41 @@ public final class PlaybackStats { return state; } + /** + * Returns the estimated media time at the given realtime, in milliseconds, or {@link + * C#TIME_UNSET} if the media time history is unknown. + * + * @param realtimeMs The realtime as returned by {@link SystemClock#elapsedRealtime()}. + * @return The estimated media time in milliseconds at this realtime, {@link C#TIME_UNSET} if no + * estimate can be given. + */ + public long getMediaTimeMsAtRealtimeMs(long realtimeMs) { + if (mediaTimeHistory.isEmpty()) { + return C.TIME_UNSET; + } + int nextIndex = 0; + while (nextIndex < mediaTimeHistory.size() + && mediaTimeHistory.get(nextIndex)[0] <= realtimeMs) { + nextIndex++; + } + if (nextIndex == 0) { + return mediaTimeHistory.get(0)[1]; + } + if (nextIndex == mediaTimeHistory.size()) { + return mediaTimeHistory.get(mediaTimeHistory.size() - 1)[1]; + } + long prevRealtimeMs = mediaTimeHistory.get(nextIndex - 1)[0]; + long prevMediaTimeMs = mediaTimeHistory.get(nextIndex - 1)[1]; + long nextRealtimeMs = mediaTimeHistory.get(nextIndex)[0]; + long nextMediaTimeMs = mediaTimeHistory.get(nextIndex)[1]; + long realtimeDurationMs = nextRealtimeMs - prevRealtimeMs; + if (realtimeDurationMs == 0) { + return prevMediaTimeMs; + } + float fraction = (float) (realtimeMs - prevRealtimeMs) / realtimeDurationMs; + return prevMediaTimeMs + (long) ((nextMediaTimeMs - prevMediaTimeMs) * fraction); + } + /** * Returns the mean time spent joining the playback, in milliseconds, or {@link C#TIME_UNSET} if * no valid join time is available. Only includes playbacks with valid join times as documented in @@ -564,4 +824,147 @@ public final class PlaybackStats { public float getMeanTimeBetweenRebuffers() { return 1f / getRebufferRate(); } + + /** + * Returns the mean initial video format height, in pixels, or {@link C#LENGTH_UNSET} if no video + * format data is available. + */ + public int getMeanInitialVideoFormatHeight() { + return initialVideoFormatHeightCount == 0 + ? C.LENGTH_UNSET + : totalInitialVideoFormatHeight / initialVideoFormatHeightCount; + } + + /** + * Returns the mean initial video format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if + * no video format data is available. + */ + public int getMeanInitialVideoFormatBitrate() { + return initialVideoFormatBitrateCount == 0 + ? C.LENGTH_UNSET + : (int) (totalInitialVideoFormatBitrate / initialVideoFormatBitrateCount); + } + + /** + * Returns the mean initial audio format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if + * no audio format data is available. + */ + public int getMeanInitialAudioFormatBitrate() { + return initialAudioFormatBitrateCount == 0 + ? C.LENGTH_UNSET + : (int) (totalInitialAudioFormatBitrate / initialAudioFormatBitrateCount); + } + + /** + * Returns the mean video format height, in pixels, or {@link C#LENGTH_UNSET} if no video format + * data is available. This is a weighted average taking the time the format was used for playback + * into account. + */ + public int getMeanVideoFormatHeight() { + return totalVideoFormatHeightTimeMs == 0 + ? C.LENGTH_UNSET + : (int) (totalVideoFormatHeightTimeProduct / totalVideoFormatHeightTimeMs); + } + + /** + * Returns the mean video format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if no + * video format data is available. This is a weighted average taking the time the format was used + * for playback into account. + */ + public int getMeanVideoFormatBitrate() { + return totalVideoFormatBitrateTimeMs == 0 + ? C.LENGTH_UNSET + : (int) (totalVideoFormatBitrateTimeProduct / totalVideoFormatBitrateTimeMs); + } + + /** + * Returns the mean audio format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if no + * audio format data is available. This is a weighted average taking the time the format was used + * for playback into account. + */ + public int getMeanAudioFormatBitrate() { + return totalAudioFormatTimeMs == 0 + ? C.LENGTH_UNSET + : (int) (totalAudioFormatBitrateTimeProduct / totalAudioFormatTimeMs); + } + + /** + * Returns the mean network bandwidth based on transfer measurements, in bits per second, or + * {@link C#LENGTH_UNSET} if no transfer data is available. + */ + public int getMeanBandwidth() { + return totalBandwidthTimeMs == 0 + ? C.LENGTH_UNSET + : (int) (totalBandwidthBytes * 8000 / totalBandwidthTimeMs); + } + + /** + * Returns the mean rate at which video frames are dropped, in dropped frames per play time + * second, or {@code 0.0} if no time was spent playing. + */ + public float getDroppedFramesRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * totalDroppedFrames / playTimeMs; + } + + /** + * Returns the mean rate at which audio underruns occurred, in underruns per play time second, or + * {@code 0.0} if no time was spent playing. + */ + public float getAudioUnderrunRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * totalAudioUnderruns / playTimeMs; + } + + /** + * Returns the ratio of foreground playbacks which experienced fatal errors, or {@code 0.0} if no + * playback has been in foreground. + */ + public float getFatalErrorRatio() { + return foregroundPlaybackCount == 0 + ? 0f + : (float) fatalErrorPlaybackCount / foregroundPlaybackCount; + } + + /** + * Returns the rate of fatal errors, in errors per play time second, or {@code 0.0} if no time was + * spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenFatalErrors()}. + */ + public float getFatalErrorRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * fatalErrorCount / playTimeMs; + } + + /** + * Returns the mean play time between fatal errors, in seconds. This is equivalent to 1.0 / {@link + * #getFatalErrorRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}. + */ + public float getMeanTimeBetweenFatalErrors() { + return 1f / getFatalErrorRate(); + } + + /** + * Returns the mean number of non-fatal errors per foreground playback, or {@code 0.0} if no + * playback has been in foreground. + */ + public float getMeanNonFatalErrorCount() { + return foregroundPlaybackCount == 0 ? 0f : (float) nonFatalErrorCount / foregroundPlaybackCount; + } + + /** + * Returns the rate of non-fatal errors, in errors per play time second, or {@code 0.0} if no time + * was spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenNonFatalErrors()}. + */ + public float getNonFatalErrorRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * nonFatalErrorCount / playTimeMs; + } + + /** + * Returns the mean play time between non-fatal errors, in seconds. This is equivalent to 1.0 / + * {@link #getNonFatalErrorRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}. + */ + public float getMeanTimeBetweenNonFatalErrors() { + return 1f / getNonFatalErrorRate(); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java index 12fc40e817..e7410668e2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -20,6 +20,8 @@ import androidx.annotation.Nullable; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; @@ -27,13 +29,20 @@ import com.google.android.exoplayer2.analytics.PlaybackStats.PlaybackState; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * {@link AnalyticsListener} to gather {@link PlaybackStats} from the player. @@ -72,6 +81,7 @@ public final class PlaybackStatsListener @Nullable private String activeAdPlayback; private boolean playWhenReady; @Player.State private int playbackState; + private float playbackSpeed; /** * Creates listener for playback stats. @@ -89,6 +99,7 @@ public final class PlaybackStatsListener finishedPlaybackStats = PlaybackStats.EMPTY; playWhenReady = false; playbackState = Player.STATE_IDLE; + playbackSpeed = 1f; period = new Period(); sessionManager.setListener(this); } @@ -158,6 +169,7 @@ public final class PlaybackStatsListener PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime); tracker.onPlayerStateChanged( eventTime, playWhenReady, playbackState, /* belongsToPlayback= */ true); + tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed); playbackStatsTrackers.put(session, tracker); sessionStartEventTimes.put(session, eventTime); } @@ -286,6 +298,27 @@ public final class PlaybackStatsListener } } + @Override + public void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) { + playbackSpeed = playbackParameters.speed; + sessionManager.updateSessions(eventTime); + for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) { + tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed); + } + } + + @Override + public void onTracksChanged( + EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onTracksChanged(eventTime, trackSelections); + } + } + } + @Override public void onLoadStarted( EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { @@ -297,6 +330,88 @@ public final class PlaybackStatsListener } } + @Override + public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onDownstreamFormatChanged(eventTime, mediaLoadData); + } + } + } + + @Override + public void onVideoSizeChanged( + EventTime eventTime, + int width, + int height, + int unappliedRotationDegrees, + float pixelWidthHeightRatio) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onVideoSizeChanged(eventTime, width, height); + } + } + } + + @Override + public void onBandwidthEstimate( + EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onBandwidthData(totalLoadTimeMs, totalBytesLoaded); + } + } + } + + @Override + public void onAudioUnderrun( + EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onAudioUnderrun(); + } + } + } + + @Override + public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onDroppedVideoFrames(droppedFrames); + } + } + } + + @Override + public void onLoadError( + EventTime eventTime, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onNonFatalError(eventTime, error); + } + } + } + + @Override + public void onDrmSessionManagerError(EventTime eventTime, Exception error) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onNonFatalError(eventTime, error); + } + } + } + /** Tracker for playback stats of a single playback. */ private static final class PlaybackStatsTracker { @@ -304,6 +419,11 @@ public final class PlaybackStatsListener private final boolean keepHistory; private final long[] playbackStateDurationsMs; private final List> playbackStateHistory; + private final List mediaTimeHistory; + private final List> videoFormatHistory; + private final List> audioFormatHistory; + private final List> fatalErrorHistory; + private final List> nonFatalErrorHistory; private final boolean isAd; private long firstReportedTimeMs; @@ -315,6 +435,21 @@ public final class PlaybackStatsListener private int seekCount; private int rebufferCount; private long maxRebufferTimeMs; + private int initialVideoFormatHeight; + private long initialVideoFormatBitrate; + private long initialAudioFormatBitrate; + private long videoFormatHeightTimeMs; + private long videoFormatHeightTimeProduct; + private long videoFormatBitrateTimeMs; + private long videoFormatBitrateTimeProduct; + private long audioFormatTimeMs; + private long audioFormatBitrateTimeProduct; + private long bandwidthTimeMs; + private long bandwidthBytes; + private long droppedFrames; + private long audioUnderruns; + private int fatalErrorCount; + private int nonFatalErrorCount; // Current player state tracking. @PlaybackState private int currentPlaybackState; @@ -327,6 +462,11 @@ public final class PlaybackStatsListener private boolean hasFatalError; private boolean startedLoading; private long lastRebufferStartTimeMs; + @Nullable private Format currentVideoFormat; + @Nullable private Format currentAudioFormat; + private long lastVideoFormatStartTimeMs; + private long lastAudioFormatStartTimeMs; + private float currentPlaybackSpeed; /** * Creates a tracker for playback stats. @@ -338,12 +478,21 @@ public final class PlaybackStatsListener this.keepHistory = keepHistory; playbackStateDurationsMs = new long[PlaybackStats.PLAYBACK_STATE_COUNT]; playbackStateHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + mediaTimeHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + videoFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + audioFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + fatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + nonFatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); currentPlaybackState = PlaybackStats.PLAYBACK_STATE_NOT_STARTED; currentPlaybackStateStartTimeMs = startTime.realtimeMs; playerPlaybackState = Player.STATE_IDLE; firstReportedTimeMs = C.TIME_UNSET; maxRebufferTimeMs = C.TIME_UNSET; isAd = startTime.mediaPeriodId != null && startTime.mediaPeriodId.isAd(); + initialAudioFormatBitrate = C.LENGTH_UNSET; + initialVideoFormatBitrate = C.LENGTH_UNSET; + initialVideoFormatHeight = C.LENGTH_UNSET; + currentPlaybackSpeed = 1f; } /** @@ -407,6 +556,10 @@ public final class PlaybackStatsListener * @param eventTime The {@link EventTime}. */ public void onFatalError(EventTime eventTime, Exception error) { + fatalErrorCount++; + if (keepHistory) { + fatalErrorHistory.add(Pair.create(eventTime, error)); + } hasFatalError = true; isSuspended = false; isSeeking = false; @@ -446,6 +599,115 @@ public final class PlaybackStatsListener maybeUpdatePlaybackState(eventTime, belongsToPlayback); } + /** + * Notifies the tracker that the track selection for the current playback changed. + * + * @param eventTime The {@link EventTime}. + * @param trackSelections The new {@link TrackSelectionArray}. + */ + public void onTracksChanged(EventTime eventTime, TrackSelectionArray trackSelections) { + boolean videoEnabled = false; + boolean audioEnabled = false; + for (TrackSelection trackSelection : trackSelections.getAll()) { + if (trackSelection != null && trackSelection.length() > 0) { + int trackType = MimeTypes.getTrackType(trackSelection.getFormat(0).sampleMimeType); + if (trackType == C.TRACK_TYPE_VIDEO) { + videoEnabled = true; + } else if (trackType == C.TRACK_TYPE_AUDIO) { + audioEnabled = true; + } + } + } + if (!videoEnabled) { + maybeUpdateVideoFormat(eventTime, /* newFormat= */ null); + } + if (!audioEnabled) { + maybeUpdateAudioFormat(eventTime, /* newFormat= */ null); + } + } + + /** + * Notifies the tracker that a format being read by the renderers for the current playback + * changed. + * + * @param eventTime The {@link EventTime}. + * @param mediaLoadData The {@link MediaLoadData} describing the format change. + */ + public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { + if (mediaLoadData.trackType == C.TRACK_TYPE_VIDEO + || mediaLoadData.trackType == C.TRACK_TYPE_DEFAULT) { + maybeUpdateVideoFormat(eventTime, mediaLoadData.trackFormat); + } else if (mediaLoadData.trackType == C.TRACK_TYPE_AUDIO) { + maybeUpdateAudioFormat(eventTime, mediaLoadData.trackFormat); + } + } + + /** + * Notifies the tracker that the video size for the current playback changed. + * + * @param eventTime The {@link EventTime}. + * @param width The video width in pixels. + * @param height The video height in pixels. + */ + public void onVideoSizeChanged(EventTime eventTime, int width, int height) { + if (currentVideoFormat != null && currentVideoFormat.height == Format.NO_VALUE) { + Format formatWithHeight = currentVideoFormat.copyWithVideoSize(width, height); + maybeUpdateVideoFormat(eventTime, formatWithHeight); + } + } + + /** + * Notifies the tracker of a playback speed change, including all playback speed changes while + * the playback is not in the foreground. + * + * @param eventTime The {@link EventTime}. + * @param playbackSpeed The new playback speed. + */ + public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) { + maybeUpdateMediaTimeHistory(eventTime.realtimeMs, eventTime.eventPlaybackPositionMs); + maybeRecordVideoFormatTime(eventTime.realtimeMs); + maybeRecordAudioFormatTime(eventTime.realtimeMs); + currentPlaybackSpeed = playbackSpeed; + } + + /** Notifies the builder of an audio underrun for the current playback. */ + public void onAudioUnderrun() { + audioUnderruns++; + } + + /** + * Notifies the tracker of dropped video frames for the current playback. + * + * @param droppedFrames The number of dropped video frames. + */ + public void onDroppedVideoFrames(int droppedFrames) { + this.droppedFrames += droppedFrames; + } + + /** + * Notifies the tracker of bandwidth measurement data for the current playback. + * + * @param timeMs The time for which bandwidth measurement data is available, in milliseconds. + * @param bytes The bytes transferred during {@code timeMs}. + */ + public void onBandwidthData(long timeMs, long bytes) { + bandwidthTimeMs += timeMs; + bandwidthBytes += bytes; + } + + /** + * Notifies the tracker of a non-fatal error in the current playback. + * + * @param eventTime The {@link EventTime}. + * @param error The error. + */ + public void onNonFatalError(EventTime eventTime, Exception error) { + nonFatalErrorCount++; + if (keepHistory) { + nonFatalErrorHistory.add(Pair.create(eventTime, error)); + } + } + /** * Builds the playback stats. * @@ -453,6 +715,7 @@ public final class PlaybackStatsListener */ public PlaybackStats build(boolean isFinal) { long[] playbackStateDurationsMs = this.playbackStateDurationsMs; + List mediaTimeHistory = this.mediaTimeHistory; if (!isFinal) { long buildTimeMs = SystemClock.elapsedRealtime(); playbackStateDurationsMs = @@ -460,6 +723,12 @@ public final class PlaybackStatsListener long lastStateDurationMs = Math.max(0, buildTimeMs - currentPlaybackStateStartTimeMs); playbackStateDurationsMs[currentPlaybackState] += lastStateDurationMs; maybeUpdateMaxRebufferTimeMs(buildTimeMs); + maybeRecordVideoFormatTime(buildTimeMs); + maybeRecordAudioFormatTime(buildTimeMs); + mediaTimeHistory = new ArrayList<>(this.mediaTimeHistory); + if (keepHistory && currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING) { + mediaTimeHistory.add(guessMediaTimeBasedOnElapsedRealtime(buildTimeMs)); + } } boolean isJoinTimeInvalid = this.isJoinTimeInvalid || !hasBeenReady; long validJoinTimeMs = @@ -472,6 +741,7 @@ public final class PlaybackStatsListener /* playbackCount= */ 1, playbackStateDurationsMs, isFinal ? playbackStateHistory : new ArrayList<>(playbackStateHistory), + mediaTimeHistory, firstReportedTimeMs, /* foregroundPlaybackCount= */ isForeground ? 1 : 0, /* abandonedBeforeReadyCount= */ hasBeenReady ? 0 : 1, @@ -484,7 +754,30 @@ public final class PlaybackStatsListener seekCount, rebufferCount, maxRebufferTimeMs, - /* adPlaybackCount= */ isAd ? 1 : 0); + /* adPlaybackCount= */ isAd ? 1 : 0, + isFinal ? videoFormatHistory : new ArrayList<>(videoFormatHistory), + isFinal ? audioFormatHistory : new ArrayList<>(audioFormatHistory), + videoFormatHeightTimeMs, + videoFormatHeightTimeProduct, + videoFormatBitrateTimeMs, + videoFormatBitrateTimeProduct, + audioFormatTimeMs, + audioFormatBitrateTimeProduct, + /* initialVideoFormatHeightCount= */ initialVideoFormatHeight == C.LENGTH_UNSET ? 0 : 1, + /* initialVideoFormatBitrateCount= */ initialVideoFormatBitrate == C.LENGTH_UNSET ? 0 : 1, + initialVideoFormatHeight, + initialVideoFormatBitrate, + /* initialAudioFormatBitrateCount= */ initialAudioFormatBitrate == C.LENGTH_UNSET ? 0 : 1, + initialAudioFormatBitrate, + bandwidthTimeMs, + bandwidthBytes, + droppedFrames, + audioUnderruns, + /* fatalErrorPlaybackCount= */ fatalErrorCount > 0 ? 1 : 0, + fatalErrorCount, + nonFatalErrorCount, + fatalErrorHistory, + nonFatalErrorHistory); } private void maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlayback) { @@ -517,7 +810,12 @@ public final class PlaybackStatsListener pauseBufferCount++; } + maybeUpdateMediaTimeHistory( + eventTime.realtimeMs, + /* mediaTimeMs= */ belongsToPlayback ? eventTime.eventPlaybackPositionMs : C.TIME_UNSET); maybeUpdateMaxRebufferTimeMs(eventTime.realtimeMs); + maybeRecordVideoFormatTime(eventTime.realtimeMs); + maybeRecordAudioFormatTime(eventTime.realtimeMs); currentPlaybackState = newPlaybackState; currentPlaybackStateStartTimeMs = eventTime.realtimeMs; @@ -581,6 +879,96 @@ public final class PlaybackStatsListener } } + private void maybeUpdateMediaTimeHistory(long realtimeMs, long mediaTimeMs) { + if (currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PLAYING) { + if (mediaTimeMs == C.TIME_UNSET) { + return; + } + if (!mediaTimeHistory.isEmpty()) { + long previousMediaTimeMs = mediaTimeHistory.get(mediaTimeHistory.size() - 1)[1]; + if (previousMediaTimeMs != mediaTimeMs) { + mediaTimeHistory.add(new long[] {realtimeMs, previousMediaTimeMs}); + } + } + } + mediaTimeHistory.add( + mediaTimeMs == C.TIME_UNSET + ? guessMediaTimeBasedOnElapsedRealtime(realtimeMs) + : new long[] {realtimeMs, mediaTimeMs}); + } + + private long[] guessMediaTimeBasedOnElapsedRealtime(long realtimeMs) { + long[] previousKnownMediaTimeHistory = mediaTimeHistory.get(mediaTimeHistory.size() - 1); + long previousRealtimeMs = previousKnownMediaTimeHistory[0]; + long previousMediaTimeMs = previousKnownMediaTimeHistory[1]; + long elapsedMediaTimeEstimateMs = + (long) ((realtimeMs - previousRealtimeMs) * currentPlaybackSpeed); + long mediaTimeEstimateMs = previousMediaTimeMs + elapsedMediaTimeEstimateMs; + return new long[] {realtimeMs, mediaTimeEstimateMs}; + } + + private void maybeUpdateVideoFormat(EventTime eventTime, @Nullable Format newFormat) { + if (Util.areEqual(currentVideoFormat, newFormat)) { + return; + } + maybeRecordVideoFormatTime(eventTime.realtimeMs); + if (newFormat != null) { + if (initialVideoFormatHeight == C.LENGTH_UNSET && newFormat.height != Format.NO_VALUE) { + initialVideoFormatHeight = newFormat.height; + } + if (initialVideoFormatBitrate == C.LENGTH_UNSET && newFormat.bitrate != Format.NO_VALUE) { + initialVideoFormatBitrate = newFormat.bitrate; + } + } + currentVideoFormat = newFormat; + if (keepHistory) { + videoFormatHistory.add(Pair.create(eventTime, currentVideoFormat)); + } + } + + private void maybeUpdateAudioFormat(EventTime eventTime, @Nullable Format newFormat) { + if (Util.areEqual(currentAudioFormat, newFormat)) { + return; + } + maybeRecordAudioFormatTime(eventTime.realtimeMs); + if (newFormat != null + && initialAudioFormatBitrate == C.LENGTH_UNSET + && newFormat.bitrate != Format.NO_VALUE) { + initialAudioFormatBitrate = newFormat.bitrate; + } + currentAudioFormat = newFormat; + if (keepHistory) { + audioFormatHistory.add(Pair.create(eventTime, currentAudioFormat)); + } + } + + private void maybeRecordVideoFormatTime(long nowMs) { + if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING + && currentVideoFormat != null) { + long mediaDurationMs = (long) ((nowMs - lastVideoFormatStartTimeMs) * currentPlaybackSpeed); + if (currentVideoFormat.height != Format.NO_VALUE) { + videoFormatHeightTimeMs += mediaDurationMs; + videoFormatHeightTimeProduct += mediaDurationMs * currentVideoFormat.height; + } + if (currentVideoFormat.bitrate != Format.NO_VALUE) { + videoFormatBitrateTimeMs += mediaDurationMs; + videoFormatBitrateTimeProduct += mediaDurationMs * currentVideoFormat.bitrate; + } + } + lastVideoFormatStartTimeMs = nowMs; + } + + private void maybeRecordAudioFormatTime(long nowMs) { + if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING + && currentAudioFormat != null + && currentAudioFormat.bitrate != Format.NO_VALUE) { + long mediaDurationMs = (long) ((nowMs - lastAudioFormatStartTimeMs) * currentPlaybackSpeed); + audioFormatTimeMs += mediaDurationMs; + audioFormatBitrateTimeProduct += mediaDurationMs * currentAudioFormat.bitrate; + } + lastAudioFormatStartTimeMs = nowMs; + } + private static boolean isReadyState(@PlaybackState int state) { return state == PlaybackStats.PLAYBACK_STATE_PLAYING || state == PlaybackStats.PLAYBACK_STATE_PAUSED; From 126aad58822b7212bf3d74a322e2b2e61aa7e2d9 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 May 2019 13:40:49 +0100 Subject: [PATCH 1335/1556] Rename host_activity.xml to avoid manifest merge conflicts. PiperOrigin-RevId: 250672752 --- .../com/google/android/exoplayer2/testutil/HostActivity.java | 3 ++- .../{host_activity.xml => exo_testutils_host_activity.xml} | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename testutils/src/main/res/layout/{host_activity.xml => exo_testutils_host_activity.xml} (100%) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java index 73e8ac4f3e..39429a8fa1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java @@ -166,7 +166,8 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); - setContentView(getResources().getIdentifier("host_activity", "layout", getPackageName())); + setContentView( + getResources().getIdentifier("exo_testutils_host_activity", "layout", getPackageName())); surfaceView = findViewById( getResources().getIdentifier("surface_view", "id", getPackageName())); surfaceView.getHolder().addCallback(this); diff --git a/testutils/src/main/res/layout/host_activity.xml b/testutils/src/main/res/layout/exo_testutils_host_activity.xml similarity index 100% rename from testutils/src/main/res/layout/host_activity.xml rename to testutils/src/main/res/layout/exo_testutils_host_activity.xml From f9d6f314e2b15e8e348fa57a64d02f5950b6159f Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 30 May 2019 14:41:03 +0100 Subject: [PATCH 1336/1556] Fix misreporting cached bytes when caching is paused When caching is resumed, it starts from the initial position. This makes more data to be reported as cached. Issue:#5573 PiperOrigin-RevId: 250678841 --- RELEASENOTES.md | 2 + .../exoplayer2/upstream/cache/CacheUtil.java | 44 +++++++++++-------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 83a323ec62..b6cbe3d275 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -37,6 +37,8 @@ ([#5915](https://github.com/google/ExoPlayer/issues/5915)). * Fix CacheUtil.cache() use too much data ([#5927](https://github.com/google/ExoPlayer/issues/5927)). + * Fix misreporting cached bytes when caching is paused + ([#5573](https://github.com/google/ExoPlayer/issues/5573)). * Add a playWhenReady flag to MediaSessionConnector.PlaybackPreparer methods to indicate whether a controller sent a play or only a prepare command. This allows to take advantage of decoder reuse with the MediaSessionConnector 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 5b066b7930..47470c5de7 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 @@ -268,6 +268,8 @@ public final class CacheUtil { AtomicBoolean isCanceled) throws IOException, InterruptedException { long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition; + long initialPositionOffset = positionOffset; + long endOffset = length != C.LENGTH_UNSET ? positionOffset + length : C.POSITION_UNSET; while (true) { if (priorityTaskManager != null) { // Wait for any other thread with higher priority to finish its job. @@ -275,45 +277,51 @@ public final class CacheUtil { } throwExceptionIfInterruptedOrCancelled(isCanceled); try { - long resolvedLength; - try { - resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, length)); - } catch (IOException exception) { - if (length == C.LENGTH_UNSET - || !isLastBlock - || !isCausedByPositionOutOfRange(exception)) { - throw exception; + long resolvedLength = C.LENGTH_UNSET; + boolean isDataSourceOpen = false; + if (endOffset != C.POSITION_UNSET) { + // If a specific length is given, first try to open the data source for that length to + // avoid more data then required to be requested. If the given length exceeds the end of + // input we will get a "position out of range" error. In that case try to open the source + // again with unset length. + try { + resolvedLength = + dataSource.open(dataSpec.subrange(positionOffset, endOffset - positionOffset)); + isDataSourceOpen = true; + } catch (IOException exception) { + if (!isLastBlock || !isCausedByPositionOutOfRange(exception)) { + throw exception; + } + Util.closeQuietly(dataSource); } - Util.closeQuietly(dataSource); - // Retry to open the data source again, setting length to C.LENGTH_UNSET to prevent - // getting an error in case the given length exceeds the end of input. + } + if (!isDataSourceOpen) { resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, C.LENGTH_UNSET)); } if (isLastBlock && progressNotifier != null && resolvedLength != C.LENGTH_UNSET) { progressNotifier.onRequestLengthResolved(positionOffset + resolvedLength); } - long totalBytesRead = 0; - while (totalBytesRead != length) { + while (positionOffset != endOffset) { throwExceptionIfInterruptedOrCancelled(isCanceled); int bytesRead = dataSource.read( buffer, 0, - length != C.LENGTH_UNSET - ? (int) Math.min(buffer.length, length - totalBytesRead) + endOffset != C.POSITION_UNSET + ? (int) Math.min(buffer.length, endOffset - positionOffset) : buffer.length); if (bytesRead == C.RESULT_END_OF_INPUT) { if (progressNotifier != null) { - progressNotifier.onRequestLengthResolved(positionOffset + totalBytesRead); + progressNotifier.onRequestLengthResolved(positionOffset); } break; } - totalBytesRead += bytesRead; + positionOffset += bytesRead; if (progressNotifier != null) { progressNotifier.onBytesCached(bytesRead); } } - return totalBytesRead; + return positionOffset - initialPositionOffset; } catch (PriorityTaskManager.PriorityTooLowException exception) { // catch and try again } finally { From 090f359895825db92ecc48c2f13429dd90e53db8 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 May 2019 15:08:51 +0100 Subject: [PATCH 1337/1556] Make parallel adaptive track selection more robust. Using parallel adaptation for Formats without bitrate information currently causes an exception. Handle this gracefully and also cases where all formats have the same bitrate. Issue:#5971 PiperOrigin-RevId: 250682127 --- RELEASENOTES.md | 3 +++ .../exoplayer2/trackselection/AdaptiveTrackSelection.java | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b6cbe3d275..cb63af693d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -47,6 +47,9 @@ ([#5834](https://github.com/google/ExoPlayer/issues/5834)). * Allow enabling decoder fallback with `DefaultRenderersFactory` ([#5942](https://github.com/google/ExoPlayer/issues/5942)). +* Fix bug caused by parallel adaptive track selection using `Format`s without + bitrate information + ([#5971](https://github.com/google/ExoPlayer/issues/5971)). ### 2.10.1 ### 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 08f4d3a928..ca8a0b12f9 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 @@ -758,7 +758,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { for (int i = 0; i < values.length; i++) { logValues[i] = new double[values[i].length]; for (int j = 0; j < values[i].length; j++) { - logValues[i][j] = Math.log(values[i][j]); + logValues[i][j] = values[i][j] == Format.NO_VALUE ? 0 : Math.log(values[i][j]); } } return logValues; @@ -780,7 +780,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { double totalBitrateDiff = logBitrates[i][logBitrates[i].length - 1] - logBitrates[i][0]; for (int j = 0; j < logBitrates[i].length - 1; j++) { double switchBitrate = 0.5 * (logBitrates[i][j] + logBitrates[i][j + 1]); - switchPoints[i][j] = (switchBitrate - logBitrates[i][0]) / totalBitrateDiff; + switchPoints[i][j] = + totalBitrateDiff == 0.0 ? 1.0 : (switchBitrate - logBitrates[i][0]) / totalBitrateDiff; } } return switchPoints; From 7e187283cd4a08f4915124ba586f895a4773fb20 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 30 May 2019 18:53:19 +0100 Subject: [PATCH 1338/1556] Add MediaSource-provided-DRM support to Renderer implementations PiperOrigin-RevId: 250719155 --- .../exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 15 ++++++++++----- .../audio/SimpleDecoderAudioRenderer.java | 15 ++++++++++----- .../exoplayer2/mediacodec/MediaCodecRenderer.java | 15 ++++++++++----- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index 5871371d76..f5d92e2a15 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -489,6 +489,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { * @throws ExoPlaybackException If an error occurs (re-)initializing the decoder. */ @CallSuper + @SuppressWarnings("unchecked") protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { Format oldFormat = format; format = newFormat; @@ -502,12 +503,16 @@ public class LibvpxVideoRenderer extends BaseRenderer { throw ExoPlaybackException.createForRenderer( new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); } - DrmSession session = - drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); - if (sourceDrmSession != null) { - sourceDrmSession.releaseReference(); + if (formatHolder.decryptionResourceIsProvided) { + setSourceDrmSession((DrmSession) formatHolder.drmSession); + } else { + DrmSession session = + drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); + if (sourceDrmSession != null) { + sourceDrmSession.releaseReference(); + } + sourceDrmSession = session; } - sourceDrmSession = session; } else { setSourceDrmSession(null); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 1553227988..08e8203fd4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -655,6 +655,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements decoderDrmSession = session; } + @SuppressWarnings("unchecked") private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { Format oldFormat = inputFormat; inputFormat = newFormat; @@ -667,12 +668,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements throw ExoPlaybackException.createForRenderer( new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); } - DrmSession session = - drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); - if (sourceDrmSession != null) { - sourceDrmSession.releaseReference(); + if (formatHolder.decryptionResourceIsProvided) { + setSourceDrmSession((DrmSession) formatHolder.drmSession); + } else { + DrmSession session = + drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); + if (sourceDrmSession != null) { + sourceDrmSession.releaseReference(); + } + sourceDrmSession = session; } - sourceDrmSession = session; } else { setSourceDrmSession(null); } 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 05f83109e8..6d0f9a4aad 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 @@ -1135,6 +1135,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}. * @throws ExoPlaybackException If an error occurs re-initializing the {@link MediaCodec}. */ + @SuppressWarnings("unchecked") protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { Format oldFormat = inputFormat; Format newFormat = formatHolder.format; @@ -1149,12 +1150,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer { throw ExoPlaybackException.createForRenderer( new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); } - DrmSession session = - drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); - if (sourceDrmSession != null) { - sourceDrmSession.releaseReference(); + if (formatHolder.decryptionResourceIsProvided) { + setSourceDrmSession((DrmSession) formatHolder.drmSession); + } else { + DrmSession session = + drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); + if (sourceDrmSession != null) { + sourceDrmSession.releaseReference(); + } + sourceDrmSession = session; } - sourceDrmSession = session; } else { setSourceDrmSession(null); } From b47f37fbcd8a15dfef38bd7e5afccd2231a65d76 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 May 2019 23:11:19 +0100 Subject: [PATCH 1339/1556] Add HlsTrackMetadataEntry.toString It's printed out by EventLogger, and currently looks pretty ugly PiperOrigin-RevId: 250772010 --- .../android/exoplayer2/source/hls/HlsTrackMetadataEntry.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java index 14268313eb..2ba3b45ca0 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java @@ -184,6 +184,11 @@ public final class HlsTrackMetadataEntry implements Metadata.Entry { this.variantInfos = Collections.unmodifiableList(variantInfos); } + @Override + public String toString() { + return "HlsTrackMetadataEntry" + (groupId != null ? (" [" + groupId + ", " + name + "]") : ""); + } + @Override public boolean equals(@Nullable Object other) { if (this == other) { From a9de1477ee9018063b5cb1f08e11c780a6d82396 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 3 Jun 2019 14:11:16 +0100 Subject: [PATCH 1340/1556] Bump to 2.10.2 PiperOrigin-RevId: 251216822 --- RELEASENOTES.md | 29 ++++++++++--------- constants.gradle | 4 +-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 ++-- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cb63af693d..3be8c4ed50 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,27 +11,21 @@ checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Decoders: Prefer decoders that advertise format support over ones that do not, even if they are listed lower in the `MediaCodecList`. -* Subtitles: - * CEA-608: Handle XDS and TEXT modes +* CEA-608: Handle XDS and TEXT modes ([#5807](https://github.com/google/ExoPlayer/pull/5807)). - * TTML: Fix bitmap rendering - ([#5633](https://github.com/google/ExoPlayer/pull/5633)). * Audio: * Fix an issue where not all audio was played out when the configuration for the underlying track was changing (e.g., at some period transitions). * Add `SilenceMediaSource` that can be used to play silence of a given duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)). -* UI: - * Allow setting `DefaultTimeBar` attributes on `PlayerView` and - `PlayerControlView`. - * Change playback controls toggle from touch down to touch up events - ([#5784](https://github.com/google/ExoPlayer/issues/5784)). - * Fix issue where playback controls were not kept visible on key presses - ([#5963](https://github.com/google/ExoPlayer/issues/5963)). * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). * Offline: * Add Scheduler implementation which uses WorkManager. + +### 2.10.2 ### + +* Offline: * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the preparation of the `DownloadHelper` failed ([#5915](https://github.com/google/ExoPlayer/issues/5915)). @@ -39,11 +33,20 @@ ([#5927](https://github.com/google/ExoPlayer/issues/5927)). * Fix misreporting cached bytes when caching is paused ([#5573](https://github.com/google/ExoPlayer/issues/5573)). -* Add a playWhenReady flag to MediaSessionConnector.PlaybackPreparer methods +* UI: + * Allow setting `DefaultTimeBar` attributes on `PlayerView` and + `PlayerControlView`. + * Change playback controls toggle from touch down to touch up events + ([#5784](https://github.com/google/ExoPlayer/issues/5784)). + * Fix issue where playback controls were not kept visible on key presses + ([#5963](https://github.com/google/ExoPlayer/issues/5963)). +* TTML: Fix bitmap rendering + ([#5633](https://github.com/google/ExoPlayer/pull/5633)). +* Add a `playWhenReady` flag to MediaSessionConnector.PlaybackPreparer methods to indicate whether a controller sent a play or only a prepare command. This allows to take advantage of decoder reuse with the MediaSessionConnector ([#5891](https://github.com/google/ExoPlayer/issues/5891)). -* Add ProgressUpdateListener to PlayerControlView +* Add `ProgressUpdateListener` to `PlayerControlView` ([#5834](https://github.com/google/ExoPlayer/issues/5834)). * Allow enabling decoder fallback with `DefaultRenderersFactory` ([#5942](https://github.com/google/ExoPlayer/issues/5942)). diff --git a/constants.gradle b/constants.gradle index 6e4cd58d09..3fe22a2762 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.10.1' - releaseVersionCode = 2010001 + releaseVersion = '2.10.2' + releaseVersionCode = 2010002 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 28 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 a90435227b..db3f3943e1 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.10.1"; + public static final String VERSION = "2.10.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.10.1"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.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 = 2010001; + public static final int VERSION_INT = 2010002; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 871a88a921564d727f8b2429631d9ed6825f8524 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 3 Jun 2019 19:09:58 +0100 Subject: [PATCH 1341/1556] Clean up release notes PiperOrigin-RevId: 251269746 --- RELEASENOTES.md | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3be8c4ed50..f284376cfb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,26 +5,22 @@ * Add `PlaybackStatsListener` to collect `PlaybackStats` for playbacks analysis and analytics reporting (TODO: link to developer guide page/blog post). * Add basic DRM support to the Cast demo app. -* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s - ([#5779](https://github.com/google/ExoPlayer/issues/5779)). +* Offline: Add `Scheduler` implementation that uses `WorkManager`. * Assume that encrypted content requires secure decoders in renderer support checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Decoders: Prefer decoders that advertise format support over ones that do not, even if they are listed lower in the `MediaCodecList`. -* CEA-608: Handle XDS and TEXT modes - ([#5807](https://github.com/google/ExoPlayer/pull/5807)). -* Audio: - * Fix an issue where not all audio was played out when the configuration - for the underlying track was changing (e.g., at some period transitions). - * Add `SilenceMediaSource` that can be used to play silence of a given - duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)). +* Audio: Fix an issue where not all audio was played out when the configuration + for the underlying track was changing (e.g., at some period transitions). * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). -* Offline: - * Add Scheduler implementation which uses WorkManager. ### 2.10.2 ### +* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s + ([#5779](https://github.com/google/ExoPlayer/issues/5779)). +* Add `SilenceMediaSource` that can be used to play silence of a given + duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)). * Offline: * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the preparation of the `DownloadHelper` failed @@ -40,8 +36,11 @@ ([#5784](https://github.com/google/ExoPlayer/issues/5784)). * Fix issue where playback controls were not kept visible on key presses ([#5963](https://github.com/google/ExoPlayer/issues/5963)). -* TTML: Fix bitmap rendering - ([#5633](https://github.com/google/ExoPlayer/pull/5633)). +* Subtitles: + * CEA-608: Handle XDS and TEXT modes + ([#5807](https://github.com/google/ExoPlayer/pull/5807)). + * TTML: Fix bitmap rendering + ([#5633](https://github.com/google/ExoPlayer/pull/5633)). * Add a `playWhenReady` flag to MediaSessionConnector.PlaybackPreparer methods to indicate whether a controller sent a play or only a prepare command. This allows to take advantage of decoder reuse with the MediaSessionConnector From 9ca6f60c3a1cd1642ee3e3a8e579184c6228cb07 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 3 Jun 2019 23:35:34 +0100 Subject: [PATCH 1342/1556] Preserve postBody in CacheDataSource when reading from upstream. Set appropriate Content-Type when posting clientAbrState proto in post body. PiperOrigin-RevId: 251322860 --- .../android/exoplayer2/upstream/cache/CacheDataSource.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 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 69bb99451e..6e20db7bf7 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 @@ -137,6 +137,7 @@ public final class CacheDataSource implements DataSource { @Nullable private Uri uri; @Nullable private Uri actualUri; @HttpMethod private int httpMethod; + @Nullable private byte[] httpBody; private int flags; @Nullable private String key; private long readPosition; @@ -261,6 +262,7 @@ public final class CacheDataSource implements DataSource { uri = dataSpec.uri; actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri); httpMethod = dataSpec.httpMethod; + httpBody = dataSpec.httpBody; flags = dataSpec.flags; readPosition = dataSpec.position; @@ -347,6 +349,7 @@ public final class CacheDataSource implements DataSource { uri = null; actualUri = null; httpMethod = DataSpec.HTTP_METHOD_GET; + httpBody = null; notifyBytesRead(); try { closeCurrentSource(); @@ -393,7 +396,7 @@ public final class CacheDataSource implements DataSource { nextDataSource = upstreamDataSource; nextDataSpec = new DataSpec( - uri, httpMethod, null, readPosition, readPosition, bytesRemaining, key, flags); + uri, httpMethod, httpBody, readPosition, readPosition, bytesRemaining, key, flags); } else if (nextSpan.isCached) { // Data is cached, read from cache. Uri fileUri = Uri.fromFile(nextSpan.file); @@ -416,7 +419,7 @@ public final class CacheDataSource implements DataSource { } } nextDataSpec = - new DataSpec(uri, httpMethod, null, readPosition, readPosition, length, key, flags); + new DataSpec(uri, httpMethod, httpBody, readPosition, readPosition, length, key, flags); if (cacheWriteDataSource != null) { nextDataSource = cacheWriteDataSource; } else { From 44aa73147663fc91f29999c11ad73179ba73d053 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 4 Jun 2019 10:19:29 +0100 Subject: [PATCH 1343/1556] Use listener notification batching in CastPlayer PiperOrigin-RevId: 251399230 --- .../exoplayer2/ext/cast/CastPlayer.java | 107 +++++++++++++----- 1 file changed, 76 insertions(+), 31 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 390deac933..8f15fb8789 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -45,8 +45,11 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient; import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Iterator; import java.util.List; -import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.CopyOnWriteArrayList; /** * {@link Player} implementation that communicates with a Cast receiver app. @@ -86,8 +89,10 @@ public final class CastPlayer extends BasePlayer { private final StatusListener statusListener; private final SeekResultCallback seekResultCallback; - // Listeners. - private final CopyOnWriteArraySet listeners; + // Listeners and notification. + private final CopyOnWriteArrayList listeners; + private final ArrayList notificationsBatch; + private final ArrayDeque ongoingNotificationsTasks; private SessionAvailabilityListener sessionAvailabilityListener; // Internal state. @@ -113,7 +118,9 @@ public final class CastPlayer extends BasePlayer { period = new Timeline.Period(); statusListener = new StatusListener(); seekResultCallback = new SeekResultCallback(); - listeners = new CopyOnWriteArraySet<>(); + listeners = new CopyOnWriteArrayList<>(); + notificationsBatch = new ArrayList<>(); + ongoingNotificationsTasks = new ArrayDeque<>(); SessionManager sessionManager = castContext.getSessionManager(); sessionManager.addSessionManagerListener(statusListener, CastSession.class); @@ -296,12 +303,17 @@ public final class CastPlayer extends BasePlayer { @Override public void addListener(EventListener listener) { - listeners.add(listener); + listeners.addIfAbsent(new ListenerHolder(listener)); } @Override public void removeListener(EventListener listener) { - listeners.remove(listener); + for (ListenerHolder listenerHolder : listeners) { + if (listenerHolder.listener.equals(listener)) { + listenerHolder.release(); + listeners.remove(listenerHolder); + } + } } @Override @@ -348,14 +360,13 @@ public final class CastPlayer extends BasePlayer { pendingSeekCount++; pendingSeekWindowIndex = windowIndex; pendingSeekPositionMs = positionMs; - for (EventListener listener : listeners) { - listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - } + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK))); } else if (pendingSeekCount == 0) { - for (EventListener listener : listeners) { - listener.onSeekProcessed(); - } + notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed)); } + flushNotifications(); } @Override @@ -531,30 +542,31 @@ public final class CastPlayer extends BasePlayer { || this.playWhenReady != playWhenReady) { this.playbackState = playbackState; this.playWhenReady = playWhenReady; - for (EventListener listener : listeners) { - listener.onPlayerStateChanged(this.playWhenReady, this.playbackState); - } + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onPlayerStateChanged(this.playWhenReady, this.playbackState))); } @RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient); if (this.repeatMode != repeatMode) { this.repeatMode = repeatMode; - for (EventListener listener : listeners) { - listener.onRepeatModeChanged(repeatMode); - } + notificationsBatch.add( + new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(this.repeatMode))); } int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus()); if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) { this.currentWindowIndex = currentWindowIndex; - for (EventListener listener : listeners) { - listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION); - } + notificationsBatch.add( + new ListenerNotificationTask( + listener -> + listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION))); } if (updateTracksAndSelections()) { - for (EventListener listener : listeners) { - listener.onTracksChanged(currentTrackGroups, currentTrackSelection); - } + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection))); } maybeUpdateTimelineAndNotify(); + flushNotifications(); } private void maybeUpdateTimelineAndNotify() { @@ -562,9 +574,10 @@ public final class CastPlayer extends BasePlayer { @Player.TimelineChangeReason int reason = waitingForInitialTimeline ? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC; waitingForInitialTimeline = false; - for (EventListener listener : listeners) { - listener.onTimelineChanged(currentTimeline, null, reason); - } + notificationsBatch.add( + new ListenerNotificationTask( + listener -> + listener.onTimelineChanged(currentTimeline, /* manifest= */ null, reason))); } } @@ -827,7 +840,23 @@ public final class CastPlayer extends BasePlayer { } - // Result callbacks hooks. + // Internal methods. + + private void flushNotifications() { + boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty(); + ongoingNotificationsTasks.addAll(notificationsBatch); + notificationsBatch.clear(); + if (recursiveNotification) { + // This will be handled once the current notification task is finished. + return; + } + while (!ongoingNotificationsTasks.isEmpty()) { + ongoingNotificationsTasks.peekFirst().execute(); + ongoingNotificationsTasks.removeFirst(); + } + } + + // Internal classes. private final class SeekResultCallback implements ResultCallback { @@ -841,9 +870,25 @@ public final class CastPlayer extends BasePlayer { if (--pendingSeekCount == 0) { pendingSeekWindowIndex = C.INDEX_UNSET; pendingSeekPositionMs = C.TIME_UNSET; - for (EventListener listener : listeners) { - listener.onSeekProcessed(); - } + notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed)); + flushNotifications(); + } + } + } + + private final class ListenerNotificationTask { + + private final Iterator listenersSnapshot; + private final ListenerInvocation listenerInvocation; + + private ListenerNotificationTask(ListenerInvocation listenerInvocation) { + this.listenersSnapshot = listeners.iterator(); + this.listenerInvocation = listenerInvocation; + } + + public void execute() { + while (listenersSnapshot.hasNext()) { + listenersSnapshot.next().invoke(listenerInvocation); } } } From be88499615d478095eab35922782759b9724aeb1 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 4 Jun 2019 14:20:51 +0100 Subject: [PATCH 1344/1556] Display last frame when seeking to end of stream. We currently don't display the last frame because the seek time is behind the last frame's timestamps and it's thus marked as decodeOnly. This case can be detected by checking whether all data sent to the codec is marked as decodeOnly at the time we read the end of stream signal. If so, we can re-enable the last frame. This should work for almost all cases because the end-of-stream signal is read in the same feedInputBuffer loop as the last frame and we therefore haven't released the last frame buffer yet. Issue:#2568 PiperOrigin-RevId: 251425870 --- RELEASENOTES.md | 2 + .../audio/MediaCodecAudioRenderer.java | 5 ++- .../mediacodec/MediaCodecRenderer.java | 42 ++++++++++++++----- .../source/ProgressiveMediaPeriod.java | 2 +- .../video/MediaCodecVideoRenderer.java | 25 ++++++----- .../testutil/DebugRenderersFactory.java | 8 ++-- 6 files changed, 58 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f284376cfb..a345976c34 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,8 @@ and analytics reporting (TODO: link to developer guide page/blog post). * Add basic DRM support to the Cast demo app. * Offline: Add `Scheduler` implementation that uses `WorkManager`. +* Display last frame when seeking to end of stream + ([#2568](https://github.com/google/ExoPlayer/issues/2568)). * Assume that encrypted content requires secure decoders in renderer support checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Decoders: Prefer decoders that advertise format support over ones that do not, 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 fe8e898b06..17591a585e 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 @@ -691,7 +691,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, Format format) throws ExoPlaybackException { if (codecNeedsEosBufferTimestampWorkaround @@ -707,7 +708,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return true; } - if (shouldSkip) { + if (isDecodeOnlyBuffer) { codec.releaseOutputBuffer(bufferIndex, false); decoderCounters.skippedOutputBufferCount++; audioSink.handleDiscontinuity(); 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 6d0f9a4aad..d636467303 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 @@ -328,14 +328,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private int inputIndex; private int outputIndex; private ByteBuffer outputBuffer; - private boolean shouldSkipOutputBuffer; + private boolean isDecodeOnlyOutputBuffer; + private boolean isLastOutputBuffer; private boolean codecReconfigured; @ReconfigurationState private int codecReconfigurationState; @DrainState private int codecDrainState; @DrainAction private int codecDrainAction; private boolean codecReceivedBuffers; private boolean codecReceivedEos; - + private long lastBufferInStreamPresentationTimeUs; + private long largestQueuedPresentationTimeUs; private boolean inputStreamEnded; private boolean outputStreamEnded; private boolean waitingForKeys; @@ -600,6 +602,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { waitingForKeys = false; codecHotswapDeadlineMs = C.TIME_UNSET; decodeOnlyPresentationTimestamps.clear(); + largestQueuedPresentationTimeUs = C.TIME_UNSET; + lastBufferInStreamPresentationTimeUs = C.TIME_UNSET; try { if (codec != null) { decoderCounters.decoderReleaseCount++; @@ -706,10 +710,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { waitingForFirstSyncSample = true; codecNeedsAdaptationWorkaroundBuffer = false; shouldSkipAdaptationWorkaroundOutputBuffer = false; - shouldSkipOutputBuffer = false; + isDecodeOnlyOutputBuffer = false; + isLastOutputBuffer = false; waitingForKeys = false; decodeOnlyPresentationTimestamps.clear(); + largestQueuedPresentationTimeUs = C.TIME_UNSET; + lastBufferInStreamPresentationTimeUs = C.TIME_UNSET; codecDrainState = DRAIN_STATE_NONE; codecDrainAction = DRAIN_ACTION_NONE; // Reconfiguration data sent shortly before the flush may not have been processed by the @@ -883,7 +890,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecDrainAction = DRAIN_ACTION_NONE; codecNeedsAdaptationWorkaroundBuffer = false; shouldSkipAdaptationWorkaroundOutputBuffer = false; - shouldSkipOutputBuffer = false; + isDecodeOnlyOutputBuffer = false; + isLastOutputBuffer = false; waitingForFirstSyncSample = true; decoderCounters.decoderInitCount++; @@ -1010,6 +1018,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { result = readSource(formatHolder, buffer, false); } + if (hasReadStreamToEnd()) { + // Notify output queue of the last buffer's timestamp. + lastBufferInStreamPresentationTimeUs = largestQueuedPresentationTimeUs; + } + if (result == C.RESULT_NOTHING_READ) { return false; } @@ -1082,6 +1095,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { formatQueue.add(presentationTimeUs, inputFormat); waitingForFirstSampleInFormat = false; } + largestQueuedPresentationTimeUs = + Math.max(largestQueuedPresentationTimeUs, presentationTimeUs); buffer.flip(); onQueueInputBuffer(buffer); @@ -1456,7 +1471,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputBuffer.position(outputBufferInfo.offset); outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size); } - shouldSkipOutputBuffer = shouldSkipOutputBuffer(outputBufferInfo.presentationTimeUs); + isDecodeOnlyOutputBuffer = isDecodeOnlyBuffer(outputBufferInfo.presentationTimeUs); + isLastOutputBuffer = + lastBufferInStreamPresentationTimeUs == outputBufferInfo.presentationTimeUs; updateOutputFormatForTime(outputBufferInfo.presentationTimeUs); } @@ -1472,7 +1489,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, - shouldSkipOutputBuffer, + isDecodeOnlyOutputBuffer, + isLastOutputBuffer, outputFormat); } catch (IllegalStateException e) { processEndOfStream(); @@ -1492,7 +1510,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, - shouldSkipOutputBuffer, + isDecodeOnlyOutputBuffer, + isLastOutputBuffer, outputFormat); } @@ -1559,7 +1578,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * @param bufferIndex The index of the output buffer. * @param bufferFlags The flags attached to the output buffer. * @param bufferPresentationTimeUs The presentation time of the output buffer in microseconds. - * @param shouldSkip Whether the buffer should be skipped (i.e. not rendered). + * @param isDecodeOnlyBuffer Whether the buffer was marked with {@link C#BUFFER_FLAG_DECODE_ONLY} + * by the source. + * @param isLastBuffer Whether the buffer is the last sample of the current stream. * @param format The format associated with the buffer. * @return Whether the output buffer was fully processed (e.g. rendered or skipped). * @throws ExoPlaybackException If an error occurs processing the output buffer. @@ -1572,7 +1593,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, Format format) throws ExoPlaybackException; @@ -1652,7 +1674,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecDrainAction = DRAIN_ACTION_NONE; } - private boolean shouldSkipOutputBuffer(long presentationTimeUs) { + private boolean isDecodeOnlyBuffer(long presentationTimeUs) { // We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would // box presentationTimeUs, creating a Long object that would need to be garbage collected. int size = decodeOnlyPresentationTimestamps.size(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index dbf5f8aa5d..a56d14083e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -738,7 +738,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; if (prepared) { SeekMap seekMap = getPreparedState().seekMap; Assertions.checkState(isPendingReset()); - if (durationUs != C.TIME_UNSET && pendingResetPositionUs >= durationUs) { + if (durationUs != C.TIME_UNSET && pendingResetPositionUs > durationUs) { loadingFinished = true; pendingResetPositionUs = C.TIME_UNSET; return; 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 7193c4c22b..33eb1095c3 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 @@ -712,7 +712,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, Format format) throws ExoPlaybackException { if (initialPositionUs == C.TIME_UNSET) { @@ -721,7 +722,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs; - if (shouldSkip) { + if (isDecodeOnlyBuffer && !isLastBuffer) { skipOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } @@ -769,10 +770,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { bufferPresentationTimeUs, unadjustedFrameReleaseTimeNs); earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; - if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs) + if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs, isLastBuffer) && maybeDropBuffersToKeyframe(codec, bufferIndex, presentationTimeUs, positionUs)) { return false; - } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { + } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs, isLastBuffer)) { dropOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } @@ -840,8 +841,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { /** * Returns the offset that should be subtracted from {@code bufferPresentationTimeUs} in {@link - * #processOutputBuffer(long, long, MediaCodec, ByteBuffer, int, int, long, boolean, Format)} to - * get the playback position with respect to the media. + * #processOutputBuffer(long, long, MediaCodec, ByteBuffer, int, int, long, boolean, boolean, + * Format)} to get the playback position with respect to the media. */ protected long getOutputStreamOffsetUs() { return outputStreamOffsetUs; @@ -893,9 +894,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * indicates that the buffer is late. * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, * measured at the start of the current iteration of the rendering loop. + * @param isLastBuffer Whether the buffer is the last buffer in the current stream. */ - protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) { - return isBufferLate(earlyUs); + protected boolean shouldDropOutputBuffer( + long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) { + return isBufferLate(earlyUs) && !isLastBuffer; } /** @@ -906,9 +909,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * negative value indicates that the buffer is late. * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, * measured at the start of the current iteration of the rendering loop. + * @param isLastBuffer Whether the buffer is the last buffer in the current stream. */ - protected boolean shouldDropBuffersToKeyframe(long earlyUs, long elapsedRealtimeUs) { - return isBufferVeryLate(earlyUs); + protected boolean shouldDropBuffersToKeyframe( + long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) { + return isBufferVeryLate(earlyUs) && !isLastBuffer; } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index 6bd4c8dd14..e1243d34ba 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -166,14 +166,15 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, Format format) throws ExoPlaybackException { if (skipToPositionBeforeRenderingFirstFrame && bufferPresentationTimeUs < positionUs) { // After the codec has been initialized, don't render the first frame until we've caught up // to the playback position. Else test runs on devices that do not support dummy surface // will drop frames between rendering the first one and catching up [Internal: b/66494991]. - shouldSkip = true; + isDecodeOnlyBuffer = true; } return super.processOutputBuffer( positionUs, @@ -183,7 +184,8 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { bufferIndex, bufferFlags, bufferPresentationTimeUs, - shouldSkip, + isDecodeOnlyBuffer, + isLastBuffer, format); } From e4feaa68f228a1fc4a35a4decef065252820c436 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 4 Jun 2019 18:03:32 +0100 Subject: [PATCH 1345/1556] Add VR player demo PiperOrigin-RevId: 251460113 --- RELEASENOTES.md | 1 + demos/gvr/README.md | 4 + demos/gvr/build.gradle | 59 +++++ demos/gvr/src/main/AndroidManifest.xml | 74 ++++++ .../exoplayer2/gvrdemo/PlayerActivity.java | 243 ++++++++++++++++++ .../gvrdemo/SampleChooserActivity.java | 133 ++++++++++ .../res/layout/sample_chooser_activity.xml | 25 ++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3394 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2184 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4886 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7492 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10801 bytes demos/gvr/src/main/res/values/strings.xml | 28 ++ .../exoplayer2/ext/gvr/GvrPlayerActivity.java | 5 +- settings.gradle | 2 + 15 files changed, 573 insertions(+), 1 deletion(-) create mode 100644 demos/gvr/README.md create mode 100644 demos/gvr/build.gradle create mode 100644 demos/gvr/src/main/AndroidManifest.xml create mode 100644 demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/PlayerActivity.java create mode 100644 demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/SampleChooserActivity.java create mode 100644 demos/gvr/src/main/res/layout/sample_chooser_activity.xml create mode 100644 demos/gvr/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 demos/gvr/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 demos/gvr/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 demos/gvr/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 demos/gvr/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 demos/gvr/src/main/res/values/strings.xml diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a345976c34..e03a0d2dc9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,7 @@ for the underlying track was changing (e.g., at some period transitions). * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). +* Add VR player demo. ### 2.10.2 ### diff --git a/demos/gvr/README.md b/demos/gvr/README.md new file mode 100644 index 0000000000..8cc52c5f10 --- /dev/null +++ b/demos/gvr/README.md @@ -0,0 +1,4 @@ +# ExoPlayer VR player demo # + +This folder contains a demo application that showcases 360 video playback using +ExoPlayer GVR extension. diff --git a/demos/gvr/build.gradle b/demos/gvr/build.gradle new file mode 100644 index 0000000000..457af80a8d --- /dev/null +++ b/demos/gvr/build.gradle @@ -0,0 +1,59 @@ +// Copyright (C) 2019 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. +apply from: '../../constants.gradle' +apply plugin: 'com.android.application' + +android { + compileSdkVersion project.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + versionName project.ext.releaseVersion + versionCode project.ext.releaseVersionCode + minSdkVersion 19 + targetSdkVersion project.ext.targetSdkVersion + } + + buildTypes { + release { + shrinkResources true + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt') + } + debug { + jniDebuggable = true + } + } + + lintOptions { + // The demo app isn't indexed and doesn't have translations. + disable 'GoogleAppIndexingWarning','MissingTranslation' + } +} + +dependencies { + implementation project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'library-ui') + implementation project(modulePrefix + 'library-dash') + implementation project(modulePrefix + 'library-hls') + implementation project(modulePrefix + 'library-smoothstreaming') + implementation project(modulePrefix + 'extension-gvr') + implementation 'androidx.annotation:annotation:1.0.2' +} + +apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/gvr/src/main/AndroidManifest.xml b/demos/gvr/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..8545787064 --- /dev/null +++ b/demos/gvr/src/main/AndroidManifest.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/PlayerActivity.java b/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/PlayerActivity.java new file mode 100644 index 0000000000..bd9c85da51 --- /dev/null +++ b/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/PlayerActivity.java @@ -0,0 +1,243 @@ +/* + * 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.gvrdemo; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import androidx.annotation.Nullable; +import android.widget.Toast; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.C.ContentType; +import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.PlaybackPreparer; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.ext.gvr.GvrPlayerActivity; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.EventLogger; +import com.google.android.exoplayer2.util.Util; + +/** An activity that plays media using {@link SimpleExoPlayer}. */ +public class PlayerActivity extends GvrPlayerActivity implements PlaybackPreparer { + + public static final String EXTENSION_EXTRA = "extension"; + + public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode"; + public static final String SPHERICAL_STEREO_MODE_MONO = "mono"; + public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom"; + public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right"; + + private DataSource.Factory dataSourceFactory; + private SimpleExoPlayer player; + private MediaSource mediaSource; + private DefaultTrackSelector trackSelector; + private TrackGroupArray lastSeenTrackGroupArray; + + private boolean startAutoPlay; + private int startWindow; + private long startPosition; + + // Activity lifecycle + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + String userAgent = Util.getUserAgent(this, "ExoPlayerDemo"); + dataSourceFactory = + new DefaultDataSourceFactory(this, new DefaultHttpDataSourceFactory(userAgent)); + + String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA); + if (sphericalStereoMode != null) { + int stereoMode; + if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) { + stereoMode = C.STEREO_MODE_MONO; + } else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) { + stereoMode = C.STEREO_MODE_TOP_BOTTOM; + } else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) { + stereoMode = C.STEREO_MODE_LEFT_RIGHT; + } else { + showToast(R.string.error_unrecognized_stereo_mode); + finish(); + return; + } + setDefaultStereoMode(stereoMode); + } + + clearStartPosition(); + } + + @Override + public void onResume() { + super.onResume(); + if (Util.SDK_INT <= 23 || player == null) { + initializePlayer(); + } + } + + @Override + public void onPause() { + super.onPause(); + if (Util.SDK_INT <= 23) { + releasePlayer(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + } + + // PlaybackControlView.PlaybackPreparer implementation + + @Override + public void preparePlayback() { + initializePlayer(); + } + + // Internal methods + + private void initializePlayer() { + if (player == null) { + Intent intent = getIntent(); + Uri uri = intent.getData(); + if (!Util.checkCleartextTrafficPermitted(uri)) { + showToast(R.string.error_cleartext_not_permitted); + return; + } + + DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this); + + trackSelector = new DefaultTrackSelector(new AdaptiveTrackSelection.Factory()); + lastSeenTrackGroupArray = null; + + player = + ExoPlayerFactory.newSimpleInstance(/* context= */ this, renderersFactory, trackSelector); + player.addListener(new PlayerEventListener()); + player.setPlayWhenReady(startAutoPlay); + player.addAnalyticsListener(new EventLogger(trackSelector)); + setPlayer(player); + + mediaSource = buildMediaSource(uri, intent.getStringExtra(EXTENSION_EXTRA)); + } + boolean haveStartPosition = startWindow != C.INDEX_UNSET; + if (haveStartPosition) { + player.seekTo(startWindow, startPosition); + } + player.prepare(mediaSource, !haveStartPosition, false); + } + + private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { + @ContentType int type = Util.inferContentType(uri, overrideExtension); + switch (type) { + case C.TYPE_DASH: + return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + case C.TYPE_SS: + return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + case C.TYPE_HLS: + return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + case C.TYPE_OTHER: + return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } + + private void releasePlayer() { + if (player != null) { + updateStartPosition(); + player.release(); + player = null; + mediaSource = null; + trackSelector = null; + } + } + + private void updateStartPosition() { + if (player != null) { + startAutoPlay = player.getPlayWhenReady(); + startWindow = player.getCurrentWindowIndex(); + startPosition = Math.max(0, player.getContentPosition()); + } + } + + private void clearStartPosition() { + startAutoPlay = true; + startWindow = C.INDEX_UNSET; + startPosition = C.TIME_UNSET; + } + + private void showToast(int messageId) { + showToast(getString(messageId)); + } + + private void showToast(String message) { + Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show(); + } + + private class PlayerEventListener implements Player.EventListener { + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {} + + @Override + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + if (player.getPlaybackError() != null) { + // The user has performed a seek whilst in the error state. Update the resume position so + // that if the user then retries, playback resumes from the position to which they seeked. + updateStartPosition(); + } + } + + @Override + public void onPlayerError(ExoPlaybackException e) { + updateStartPosition(); + } + + @Override + @SuppressWarnings("ReferenceEquality") + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + if (trackGroups != lastSeenTrackGroupArray) { + MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); + if (mappedTrackInfo != null) { + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) + == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + showToast(R.string.error_unsupported_video); + } + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) + == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + showToast(R.string.error_unsupported_audio); + } + } + lastSeenTrackGroupArray = trackGroups; + } + } + } +} diff --git a/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/SampleChooserActivity.java b/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/SampleChooserActivity.java new file mode 100644 index 0000000000..1ddf5c1517 --- /dev/null +++ b/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/SampleChooserActivity.java @@ -0,0 +1,133 @@ +/* + * 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.gvrdemo; + +import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_LEFT_RIGHT; +import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_MONO; +import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_TOP_BOTTOM; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +/** An activity for selecting from a list of media samples. */ +public class SampleChooserActivity extends Activity { + + private final Sample[] samples = + new Sample[] { + new Sample( + "Congo (360 top-bottom stereo)", + "https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "Sphericalv2 (180 top-bottom stereo)", + "https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "Iceland (360 top-bottom stereo ts)", + "https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "Camera motion metadata test", + "https://storage.googleapis.com/exoplayer-test-media-internal-" + + "63834241aced7884c2544af1a3452e01/vr180/synthetic_with_camm.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "actual_camera_cat", + "https://storage.googleapis.com/exoplayer-test-media-internal-" + + "63834241aced7884c2544af1a3452e01/vr180/actual_camera_cat.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "johnny_stitched", + "https://storage.googleapis.com/exoplayer-test-media-internal-" + + "63834241aced7884c2544af1a3452e01/vr180/johnny_stitched.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "lenovo_birds.vr", + "https://storage.googleapis.com/exoplayer-test-media-internal-" + + "63834241aced7884c2544af1a3452e01/vr180/lenovo_birds.vr.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "mono_v1_sample", + "https://storage.googleapis.com/exoplayer-test-media-internal-" + + "63834241aced7884c2544af1a3452e01/vr180/mono_v1_sample.mp4", + SPHERICAL_STEREO_MODE_MONO), + new Sample( + "not_vr180_actually_shot_with_moto_mod", + "https://storage.googleapis.com/exoplayer-test-media-internal-" + + "63834241aced7884c2544af1a3452e01/vr180/" + + "not_vr180_actually_shot_with_moto_mod.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "stereo_v1_sample", + "https://storage.googleapis.com/exoplayer-test-media-internal-" + + "63834241aced7884c2544af1a3452e01/vr180/stereo_v1_sample.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "yi_giraffes.vr", + "https://storage.googleapis.com/exoplayer-test-media-internal-" + + "63834241aced7884c2544af1a3452e01/vr180/yi_giraffes.vr.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.sample_chooser_activity); + ListView sampleListView = findViewById(R.id.sample_list); + sampleListView.setAdapter( + new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, samples)); + sampleListView.setOnItemClickListener( + (parent, view, position, id) -> + startActivity( + samples[position].buildIntent(/* context= */ SampleChooserActivity.this))); + } + + private static final class Sample { + public final String name; + public final String uri; + public final String extension; + public final String sphericalStereoMode; + + public Sample(String name, String uri, String sphericalStereoMode) { + this(name, uri, sphericalStereoMode, null); + } + + public Sample(String name, String uri, String sphericalStereoMode, String extension) { + this.name = name; + this.uri = uri; + this.extension = extension; + this.sphericalStereoMode = sphericalStereoMode; + } + + public Intent buildIntent(Context context) { + Intent intent = new Intent(context, PlayerActivity.class); + return intent + .setData(Uri.parse(uri)) + .putExtra(PlayerActivity.EXTENSION_EXTRA, extension) + .putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode); + } + + @Override + public String toString() { + return name; + } + } +} diff --git a/demos/gvr/src/main/res/layout/sample_chooser_activity.xml b/demos/gvr/src/main/res/layout/sample_chooser_activity.xml new file mode 100644 index 0000000000..ce520e70e4 --- /dev/null +++ b/demos/gvr/src/main/res/layout/sample_chooser_activity.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/demos/gvr/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/gvr/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..adaa93220eb81c58e5c85874d1cf127a3188e419 GIT binary patch literal 3394 zcmV-I4ZZS-P)d>%xF$xW+t8d`u>^S&KV-GPLkW5 z`RC5^;-~lC!ku)^Ohlv?a=C@{LON&3(GBWZt}HiPtth81qyN7FauI`bxyAo)XVqgh zW3>@#CO*5}T%G@AK=NDHsZ^KMh4m_H1?vziij`JcTAI%)hQxiE_}?Ls_Z3mLuDVWX zS^p(KZspvdA!{OQJ1dz7PLR=Pv`VrZ>R@dXbv7*LzHZeSkZU=U@4#Bj$|wiLsE8zP zjUtt*CGx4WEBIUu40Ve(I+Sxi*XjmH{ms413S+6EC@KJ(K2_|i^|BRCO@`vH zvKRxd&J_Bf3hDvqqb-nZf%6zE79A0tLZWISqY6}PadGkEtSUjOQZP0c44yD!6&$LL zQuVDUFE6hb%j)25wdHW5UVK#tVXDp&eZ-Zrva)~XIwT%PQA|wC zQ!Fty+W@(UYYhZDmU+u5&ZUyjVE|K6yQh$naG3KcPKA`8pMU-Q`SZ0|VvIHd;(Glp z4aiFJDZnr(!;!a1$%=Mr;7(6Z58@*!eboU8^D8MSnPbIl1q%TL^9me9h6}%uu_((K zpbYG4bmPX2g)EbWH|9L8FS+t z0*O;;K|ujsym--*CB|+Zkc(LJgvwdVae#z^hLGX%63mDWSwto#Chopfz^>G_m}*;L zJ=$83e}78)aWGx+8%kFf4yEK@2Ac-8a||ihLy0$5b~^U>}5qrbp87E6|4$wR>8H{ z-0E||G#Tx+093fOKZSMxkjk~|C0{Dq-oMgv7iW9X*87K1+yF1i`T8p|BqiBpM$5>^ zNME^fWm8s}#bX8Q$;ru?Ago8gu^mwO8$$uYq@v#&Ql?DoM?)y+8To$()PE?Y4;e%m z!+j}#&1zB#@->CTlM?ZCKa(X`R0QOTK*jF0Rl1i}fDR7uF$?Hme;?$f)ISE(p6=2W zD*}?roHB_D_wLmsKFP82{w$CglsWWQKQhrBK76a_he)DxI2@lu8&9Ku?{_eZ)JXrrSQO{n8KV9UxGK;hVbaIS025Gg-0}>iKm~wym zkql{RHm!*;o9*7cyDv+(XU`rtmJT=n`Wy$USEPGs2MBI62$o?#KtdTKM^M4$O=K*p z(y@wy{n^>GXWvDrkxbO_$Ats*3S`zA5OigMmra1;`wsyGieZ|3 z)Nx4qnw~#&<`|fl9(8) z&Wd7VV~;S4+awnWNcZx~FW=oDvceq8@zk!FyRc3CIP2mM>r4fHNhF0-6De`*H|{w71NSqCG{Nv4b+kj;7)> zXUxxJoL+tP)ut>R*!&A9C@82U1IpG4P{yJ`w7-v!#W7~*W4;)OU6nCr6e)Rm=2p}> z-+%x8gRHWE0s;aaU_b@x4Qf?X7=46Jjq|3>t*i^e$J+bY9S~G>;)JPSD)Y zv&nnOB^ve1ar)BhE85s{xJ7`%9aE6|$;rv9>;>qP(LdR?)#zvW0y1l%9~ED^Sf#DTsZ*ymf`xjk0l8s$&HIqIK~ZZ! zGd~QWO)c!dSJ(m^_TBizj0*Vp%9esGu~-AoHW zzKhP&U!51;63%Zgzd=5WFIFh$v*-f7HETJoeqJB0_z#sL3=x^4F8}ljnP7;j|b|-}n27M*Mynd2Xri{jX7B z%nj6U4=KEZpuKzdZot^VfTfd?H;|mo?&6(nKNOM&_*wvxn*7y1dTst%dev_oP5kzN z$-E1;MADg?Ire-uj}0nbrlPmse%lLUm+_}U43e5Xd-m;|u@OIp-6gF^6F^pI!s@FO zcqxra6vZJgFPu1W;sB4?17m0D)RH$+ayGm7r=NcE=7)$XE3!YJVaqPlqQeQ4U7!xY zcC{Yt-syu6J{XNL|MABk?kt^J;-;*|jT?6d7ifIx(xq*f{#XWk1NtyHmSWQj9Qqob zh&_QAE5^*ym6yCx3-QPmM28O_-Wf-b!itRc05p4J1j;xLjB4iMFp@K1z<@3oE1&fBu~; zU1btC6~#&4sZ*zW`P!P_Ck|nAL<44B?Oa^0NiaT3FVQdi<0w6^82Oz62d+i?_U+rn zV{0SEsApN9V0L_|-K0sAC;82XC`*3wRLB-FW!<$(;k+FXO%W;iPW^EU!o`IP7ot5p zJf6karc9aQ&eGjV;>M)z;))uM9Xqx!@6&RK$n4aAH7Sp#sUS^TA5QxtGf8ps?=^^1 zbaZqs?-mAPOc)zWXA>Z3YBuEsxwf(#N4f5G!HhQ59K))sIyO)i}~ zcSgmnIW|q=!|iOb6Rwt!WI$%Yt#aOYR>D^yCj!x-MZD~YpYSCMh&=f zHyVvAv6V_vk7A^XO->b>OH|7jbYB1;n zFG;d~|NZyRxo?l7kD{-7mP3>N*=L`V-a`Z|I|dA!Z8J6H}*79TgZDxIQW>DhZEMgjFHQ zeVrnbk^hH3)KY(jyb8*YNT`dS0u$;+8Tp~%R{INnDEqfXQ@iqWGuDHw$NAfR!Ozcc($b|%zX}Zv-NcW1kML3u z{G>M=c{sLhTDEN2S15}*gtef~=4b=L)(+s114i6@?DP``ft zMmP%=;F_~q2sIa;KEFc)3NPzrep1o{^^JjiG{v$$j^J-@V^i z?tSUk&(8}0g3tz_h0tFK?d@&p*?!OJ!omBa1-tofC|W35I5WB*>ZWZl_4Q1F~p> z2qf?zUf=9~F@mUZ0&V3u&LYLAtrMXd>XY{gwi-c!0zW`-|1N?q762zeX3L`s`e>qO z@&95P!`0Y<0KXD}bArJ+A3(b_)|Z7zi|$wYO+xq}cDRq9bvA3(K!JCXpqc;QWHKG_ znS?MJU}s7Op4TrtNqf8)7U=EmJ%$j1`b0wLJlNTO0x#^;K2UQJm+g(JBpsan&jix3`b2=YOY?whyoAqienlse65;|} zg=H?K#52ObgK_q21hE$(&54n>SaC5F4jM>qSMDnA-SiB2NnpU4k&?=xWKz)7v0*6%)hNhmLp7Wlx z?~cG#*&N6mrEsufx!nGJ@97y(_e~6(c{2_k%t?T*6Mq0_H2UNVYPI@LfiDEv6NE;i z8IO$&dq!@GDj;!1hT>uw(rz?WA?6K}a=Z#Z9{saY_3XcPgY?hb+Gi`Drbm!(^{xEr}lc1%g3tnjY<)1@ONr~Ic+}he&w|)Ee;dTOL zWo55nqIl3(0v*Sq;f6vES)*OgeC4!QNr2s%VecN71lrr%)hQ_{VRizgrKMvrfoY`@ zFllO_YDFyMM8tX2yIL0G7XdPJQ=^Cg`l~J@BV(eSKw)9w_`VRRePDofE0my|yuc%Y z2Qvdep!q+S1X5E|Bkct8^72Mu5>rbha97<0@{}q_IB*NLCj0^?!xLQ+DF46@3C*4b zoktG4ln{n|&F%^Fi>ynd`Ul!JL198)!@|D2S&FA3cm_)Zy${7=H3#HkkJ^O`r{yZ>Vya2e1 z@fb!M8ykxe^I%~i7mTqr#TNo|j#R?YsKx#zvN^HPnVJHe!5}G-RaRF1j+jGi*jXtN zR8di}jIY1Fl@me&3w|nc3sxDq5Hi9NJQC>Gx&(63kqy*F?6X?sb<97?@o$BrHE zu>>3}ET@NJMOq-GoObqspJ!|p5%?iyr$d|7Uu8jB)Dlo9%!Tf}T({+dw{p6srlt_F zzUZ(O;QgSesAxI&sJ!(kBWD7NL*=jy!|Bnt7Nb8bfh|dUAnw-+hc@wPx8bO&2~7HS zUv^|x#AwBG2ePxXNj||2+e&_WHg)RMH*ksWdfQSu6Yyr+F5ZJitg>AiNpj~oTFzmYJyJRcG;3cY{-eyJy4@J3*s z@($!xwt~sb1?*S_wSNn-y-om6oH#MS)2ZCRUAuP4aD(5=juk~xs@1qA5SLm72UX3W z>oNu8U<;MNQRLBTwY{4+ZP`pGZ(|fXx> zj)7=nuUxru469We334imYER(3fb9hbjX|=(Xh<_Zjxg9Vt}hpRkVu4)4lzgcu;UqP zpoj>7+xlv2YwJl*Pv1#n`UPGnr^v|2=SYXRZ-}q1t}dn3pa>-(V(H^+C;pA54LJ2h zed2jf>6$fb5@{?Nv(K{eL{}#_??$T1T6|r6Xn-wpwSS^C~Hn~t= zN~Q8U?0YheL1R6UpCPaqOp_y$NYp}9s2#l3(SVF&qA2KL+F`6==^`kyys$3kdoH`0000< KMNUMnLSTZLL=Vyc literal 0 HcmV?d00001 diff --git a/demos/gvr/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/gvr/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2101026c9fe1b48ab11a23fd440cf2f80af2afae GIT binary patch literal 4886 zcmV+x6Y1=UP)NZw)cXxMpfx5T2C&3}PVZZ;p-o4i2PAJ@j z+=Q8)wZ4!Axsm_BXYYOXxj|RrxHs+%BJu*>GCn21kMjaLPsdZ=m^Y{(+<2>%EO28%%wuqUDMu8OH$8%zKu7G4&}qFQk04YA1g*$9*T z-fAmCrB}H$Kq34SU>Gp7@Ek5m5M2v1I8?C_X8FB7VgIvuz5!mq7wBXR;06n?h|Qh^ zpAc-s4G;!&GQPme(+%+E*a@BkO92bdVTL>$4o@VHrSfOt+~Do02C(pgS3oS_4`l^Z zo>0t&81Rby&*~gy9`Es{e^8wD9OKI!Tp)18WVKp2jmH3M;1&be{m1d9 zjuM7eWu?2zR$)RxLBX-u*w`n5Q!~LS@l#V%#hHJEh}<7?G!99^gxuWRqXRF(=;(_=tajB`c+L4k@u+Sx9J@^Gl=@-gH zbtLBlx_R@aA5c{psY<%p+1bzG#bO+-^QBvxg}5s4BkSp3$(EDi>G?o{CL$EY9zA-r z3SiC3!~idWh;ewGB~Da4FkquP1DLI$BwbxbHcOsIw=OV5C|U`vyjeG=4PYlx_v#6Q zW1pr5FuUI*DgFSSOY+EkKo~hWIdtX9mA;@;qG0A=0pN&_+$2GE8k#f#5uZfT(GWp_6rYpFk-=oLzv*V}fW~yFWgb4PcD=P|8`jfTZkf%}Uawq@;u?Q>N4ay5-B4`v4syZWv*J zub3*RVMCPR$CU=e{Td|1QbqL0AaN|E&kCUZuju*rB^tn(-W*7IySeS{`Y?3x;K6=E zTb$e#fsPS33|Bwi=x0n)p8c*g;P&tk)dsAoZ4lKF&m7eTFy@GGD!6pfrR{2T5(xUv z7TPMSp+AJ5)~#FnB5s|~(txB1fh?Gls5gM~L)O^e$(mC3ZEx1__U+qeDpjiVAmA`m zGz4+=bNTY+`Z&M@v@!t6)1THi3UiwQjIpPIa+WV6DKD?g^FT^Uih0qZMJywMn(qh+O?~XvK&=*KE2U;{)V?3 zF$%RZAbUC68A6DLj#Q|19>{a*nYdzQV`uNEueJd~DO z+BhckhB^k?(?U-NTIp$56Fse|8Ae+h7$`L`&@ls;&|u1%GMOZ!(Ww@-NW^E)o?Qfu zeOYgyE0TaQDxw0~?hXUE`L?r*x>>NPu7NK9BZSfdiwXzf`FCZ4u}^QxUB8Y{(m2!| z#USL@z0mkUzI>&K0U|bqhK4?jHfkge(A)s?`~W%>&_k8@+Zq}u$v=oi(ggEIA5i{@ z6Atx-Pzjyy*s;Dj?}UiDUX}Q3Yw9WfbrxTH zGDnRhDLbpw1`r0WBaQy#lTWJse|NOV*5jHrYii@MNnAd;$^Z^G*MA76P0byP`NunQ za&;e>BS)wzMX|q(CdC=xy<^9YhIp{a)dnPv3#47$oU3?lzZC3N@fpkY>!mgz zIXT%papJ^QLTkGQY}vA<84k#Fl>u32w%qA@R(HOfGBDlMZItdC>mzvs$f5t~P-AVCXiQ+4&2H3^>xZ zhuaOvnlN5f5=NURB_blCd9enBhlf|eI^Y(UPOdb7=`s5}ef{ZTTGZq%b?xXH28wyb zO<^b`h_a_m6^k-eW$3M2x1zDusw1@KYlU>U;f0uYtY#JJ5>I+yG`; z_Xt%pVI3B5QQboSkdza!NJ|L_EHmLv@4Q9%`}aHAAr*3pUXW--JPEvUm4ra26F0&D z{1olzhmk1exXXavlTOgWX78}LszcKDMh4p56f>j#o!@P&YjD~GW0-+*mM$SFH`meq z0B*(+zBjNG8b2bmRyN~wVv|5LiZD%nOl%nafdzu)~bNU%tUaz0q4KNMtPkFm`lFe!@^DW7^xH!{i zpMBOlb<%JH2}wK6vtA(iO1-dz)`fadYCE$ z%%6Tt1*cDIa(yT+bR&F|{zIWH^1%H-$A}w7p7him_mC#m{pg?Cg1PNY# z`DHN+p2har68TB2T2w*51dT4W0V8^i7suFbYlMHfoE9{Hr-%W(xt={^I!R`;OGQ`I z|5g!xcJJQ37MeaMv;{gwz;_s7LPb3I@wIE$_7zXUVaU}6^u;1;kE!Rh z(|lYpRaGPn=|7p4*Xbh+po~F3lI7M-w_Ki%|0qLa$BunpXv*>5UJhV53_V!2YSr3^ zT^G17q{T{j;MhYntK(E&g7mY_QN$O^M1@0h+k7a_cpCreQ2H`rl?XAV z68_C4mudT@bh1j?imo{OZRo4fiWMuy3oV7FK&M39DB&>lXy3kl+hI+R#3iW)1~^67 zyo)sVXd-3im5^YyOTH<7etzAAmLGle(S1OtgzprAuMxt}N~cbpnrgkXGW2KzM(l{D z7^B<&mjowY8%|t89-SvNl(qEIsTsmCv2f_!yLTr{rDUE3o@T(OtFO_im`t||uRQzl zzamm`_WxaI*uQ`O`+?3L;>Iq*vCf@4KZ2iC_V3^SD|X7n3wVqH{TE%P4d+s+z^Y8J z?mzoQ<88^3C6k4AvW8wj{lL@_x!_4uL``s2*JWN8xYGdrTx{ALj3;A`R&iea#tXvs zwd&QYw-VZM?Oz;x$1cJ-Oe5i7<(V^QzMqwqMf}BWcNp-~mRocqHAibv)o#2Ku6+1H zXa)@*<+I}L?{wFJX#v?!HTPN=%$$@O+{6|>-FFE&H+fWBnZPd z#&T>M-D}(44PNMMW6jXnw!O9;INP>u1(|%heRnbt14b z%;Lo@rkE*HrZ6tZ?cTk6A5BrbgurH8$W7n8Nx<)a%hSQ~hu;gn{w};1_&F?ie(0fx zUWr5^1Hfwd@Zr|;X&s6kJa}-Lo{P^of zJw)x{Aa-#5_19krHil8cJ{~AWaV@1ohYsv(snrbMVg?%`n*RSiP4T)0OGht#?k~U} zvEl?QV@Ug|tcrm|EgWDaO&Gy75fbYi%YJ9S+CPZ$$ z@y2sCSo==oM$FWPHe2egZQ};S@251qB5eUAlC^2@Uh-&p%9~ z!k37Z{%eEZQZF00a_r^l)2APaZPUW*LBs^)q~fsU%&dzH{T4b zHc%6waF=?K>B_m@V8x0RYpbfNhGG-8brppTd`B_X4eL-*QBkDlDslAGQ%{|)t&h*> z4o#fk90C6nrmiJAp<5tASC#r_wKO{u8?eP}>R>ivYiKD}oLp8`mXn>GU7^?Y>FMeH zZ@&5FOP_e+iFfEJ;Rq29_`#g9$6`D>pbHAIh^{@H?`T@#a&$&_Y`_+5GTYh&xsAS^ z%FD}(tu{o43>h+R@#4id$PsT-%2;cL?7vtbN}^xTks(4RviXdzB|2#p>qf0$O+Z(4 zMt5xR8*==Iq~zS(+$^mS(LmRTqy75z8>>=wk*fKVv=KIvcFy<_qI5#eDR7g3Hn4x6 zmkTlOo`1TP=y2V2*InGFPoMGVhK}fp&gdTQkkf>bv$%t*bB-G4a&tsP!}z`n7cM+e zy9ZWj1JkPj!lH(&8IAzXy0kz1iGDQxl}JO{--GQ+I*w+XB3Z8>6WP+CAG)9ux}hVw zdKuYx0v2&c~z1(GLVH#Wc!ct;U{-SIo<&2pwgMf zfoA&Nga@l36&A8;0Mxk7vAwUcG!^`Y-&!8ICaE@0ri|jx?k-uv5rmFW@bA2pnr1XB_`0cAr~1`(4QCXBG>Tj>W)rr~m)}07*qo IM6N<$f{P}YasU7T literal 0 HcmV?d00001 diff --git a/demos/gvr/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/gvr/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..223ec8bd1132aab759469bd5ad2232f35f7be2c2 GIT binary patch literal 7492 zcmV-K9lPR*P)7bFjo3yR6hYmf3aeOcP}{a`bNw*d|0U;cCTESAhdF=p<#BND ze(#)nSIgQ+Boc{4BJn&=2L@r*gF)y(i#kyntnL#ek1(4lg3iwuHMt@n4EL&P$1W2%4KN`sv1kd9&jD}1l8iYZj<4mYg_}n>DmK;k!doC zK-8Ytd#%B2upQh1F)j+>AQoX0DsDZf*n~wmwTKm?d)ZF+)|%ZjwK)+eUDYNJOanW? zn;L|%l_)+zyns-GRb8`&D)ol$bt=dUuPTW^d~&;k)!;FcCKVEfWJxcs63RuGP>*R0 z-T9A11PV@^$>1O;W0l6DU%!{(q++pFS4Lx++;Sp`K)hAW0H;{BQI*EaQfwuY3XO3l zp9c}B;pl8_FcBQ(;;8nNBe+~78uKK!*3(6xI$+}T00;fvkT_nBW3h$OLC}NEkqA^r z)zLAD%kfz3Gl$w#bYMHUVww}3RU;9I_4i5OP5`YcS6bU$*4E6Z3zQEuVm(mt@2CU> z5?Q|aSL4y#5NK~Mg#jwlFZsOPqA&&6XANTp%zN^pJEs3?(0 z%mW&rxy@aHc+cM0GF3;ak!4w%G)>b%=w(ATBu}!%CsIgy2-ZR` zK7IPs8-!AcLXii98jQzpp~)i>ef#|R^Dq#&M1i&~l4lSef75~|BzZ(4RQx~h-n|uaCmfD&_xA;bbBL6fpgL3ffTX{bTL=(&!o5T$ReRGIh+p* z&-^E#W(q)kq$}0MM!=PkkpYpBks35%nVB%rjB%CERL7$UcYC{>eJ0L15>N5JQj0ins zbiIe*L6Nr7Xx+mxDP#))AkHpoK=;T4^ipoAgNS^TnVCtU?BC=&W9vgki{|X??B0Cc zqdpUw{8)5QUN`)lC(a9fDlYz29zjRfg}J?J8x zP#@)}T?^@O1Qzbzy<4P=%;35YimWD_x{u-jirH?7pg4eAbH4e1=l}_OERElh{3n7g zI!N_(q36$^r|#dszb*OP+1a@aniguRYLgZGKH%@q^XN-kNV2ttj2(_3B_^sa zjG>>Yuy_xBUXgrXSdYx0sxuav2FSY)6VHkSDD~Jd@NBQ)fWo)a-8GQuJk(%tQVtJ; zprQ8pfRzEV8b$`mBZot_rw3q)qb4y;?F~3{=FFGmdlJ*d7SXhYQCf%P?v4ELr3`?s zY3#^}M%mpdwuOG3Y`39sXOb<3J_c*S05MCMF&%QRT~$T>2$v2=Uuo21H~GG>Fji>z z+OcCtRs6&cnRr$zK-um?!LyylXn@Z2&_J%w>$AE?KU(A8Dd1x{05MBjwF0!ypDA8U zlODobJ|f1CA78t$Fji>z!rH(``N9_{6`+^DSdjyTSbPClS*)WaWo-P91AvyAwkCNKpWx-{HZ72RD#`O1v<}D;`>egoL!iPnO2AFGcS_^B6$F zO%OmFv$llv4eLRQic(ZI_UzfS4Aj6qi3KVu8eB+Z*%zjB4uFeYL#JSXsAZvAfS4uQ z+d|f{qoB*oP(o2rkL4RUckbLIVgb1*Zzw=a|CGZxxCwCSa48+2EZ3owR;wByW+@ZK zL5{y4=q2fmhA~R#gM)+j5eu)=K?Uj`TU%Soz$##i-%lj~kx|psBmiQTwqz0Hg}$-d zMo0$%%9vo%LBvEsI_S0fXXD0=b&;DFvi{Ml0icwltP*Vk05P+%f{e|ZK#Grl6U|jR zfEYb|`0%E3hn|cJ5DQ;ru&qz8yVi09v?6<9ROoYSVLAXYOa9IQvRs@&my`2P=^zr) zbaqTSc<|uAh>1OW_LM_2%Ww2Ueq#cDS=h)`5V9p!e(`Q6w-NOZ(w_&^~%pq5#p}fmaoRudnYqV!{xB$jXy|#-lqs zn1EIzK-V0^3F@;Q93b>V8%VIUEYO(=R+bPu)JiZU1{jTv?Ai&sq@)s^QpBrZjuI0F z0kmSp3erH8@B_Wn3Q+uROYrI_Mob6et`g6{QK%)48Ufi}o}kam6P}6X;pe=5{W=ZX zS0*;l%<>!k@IUt9haVC^mGO$+Ye=A~LKw~R+!|vG?ybeZbG0`MN$RlFnK(9bSSgg9G#8Qh5LF_K;|)WMDXL zku^WlG^7xKhym=0J!;snVLf7l7ZK%r4qS+YjkX)Xwe|3~02LLWt(!&cKw3ipVnxIXM~)onj__nF1)v=P zufX=uLzuF`ANG8-w8SBL{*~zfl;!4XG(gxF%~`s1Xb<^Kf(X!q{j zo$(W-vn)(005Myz#2t>kJGI0By7w>B5Ybc@LKP1p-g9YzZW$iJJtX@oDw?LyM{$B!Q;jT<+vN1*_*NyM>Z$2P+r$O|O{ z#BA)gE3jqQI&f_%HamOrg}TF7+VW+f%ghu4P-tjq^p{_L*@4)gb_SXmGqe1^U;v@j z!Ex45*2+*aK+LAE@r46}76=3Ad`Ab?L{*pB+d}5ygP_aK7S_tZ;lfAAL?dE@7(p{* zW|sdKxGY+>Y*`VXwXqw2MNI&a*&TcUOXmFsC%R7(R*_DyQV&GwKd%Dq^XEdxS|v%M zbMhg`L_L`e3p36#$DN4&QZ0NdkS81GPzylJM((@;$9qi?0+C-=dx##a?80K|tQpjW zFR{B5FI~FiOPh+!1|J~QLY47Z6VFEPR4YJa8|*g=3uj)~Ho1w@hf%KqiGH6nQ-b9i zm;>_m0zEppBqkEs(Vnws&z>MAWH!vX0L`5{w-P>UU@CsCQhk$%oRL;~tB;eD(=WtCQ2?rrV~iRAP*fq&FnI1$ z)%uN{%6RgXt@>u6IYagMP84u-beu{|7zEJFnKLO1tG0LV-hTLrI1N>00<>oAE+w4{ zQ~qfTeifQ(K)H(-FaCm<7&B%}#X({RZ3aFt4xCi4XWU?0p8f@D+8oc13)`J|G{*Q zHq(jJEt^4#i+hu|*HZ=-Of{g!jT=`cHqZ)P7FZLiiqE=FpFZ6T0IC3JjN>)jeZDgZ zfKnF=I1?`*1H%mj+}+&|5(_dD76t({c<^ANt+v0v|0n=Zg$7!<$W?LIyD0xw4!(fK zW6iK_+qPN60x@AIK;~b4^%V)}8uRAO>m^B266=X68KBjZ_Nf-2WCuIQI)04xu9RSU z0HD`lrj8gfVj!^~7ZJ@30*J&??b@}g!{w6jeaPyAc2nSL~%^s(0_+)tP`cbXk1<`R7$gJRLf8 zXfP>qRu57tK(khy6$21fl_&jr>Ho(@wqAy_!^5{_txt*0?|9SN2k#70& z<->^q3|~w@8@$Z%bSmkfnrJOCjNN6skWvBqZr>en?li_|fR4R44eSm*cd@NC)B3)=f$Wh;04vtwsa% zm?higV zY;U+8qFrk`Y8(1kV{GB-vkfy5mc4iOIE<|a)(g9*N@z+0L<%GRJA!h%; z1+aI}LfAHVHT*Q~FpR_0+}K@bk@sV`AC;jVP&mD>goFgD{Wy`Yu>)V0IOehbm~sIsd7JQC1Y8VD085yUbr`-wLt-g?X#!}S?{j#OEMTxpf$vGZfB(J?r%{KKk2`kkm|r{#0f?HfLrg-hc$;j}ghK5u#m^g9bw|EI- zHhp7yS}Fm^{_sQCcIhR=r?d7$sWDpg{)-%qMq^DrG$8X5z6+H@tKJe9K0CO&x~{`A zIFYRifR_3^fv_0i1MI4q_rbRi!uIXkw~+5-A66%y@^>HcyHG{6+BIs_Xob(t_>%hH zvCUB;S=qkeC?ViXc8UQ1$YjvzOdl3bzl&ZV7f3#neOH@&Y2fZ7UQpMqTbH)KHr={) z>x*rUS2Rx|8luzp6O009#IZ0qaU&KobEySWdf~x0Fwsei=uVwF4I-bR?-~me&PE98 z8Z4BpT)A>AruKT#dMG+Imo!VU#w>opF^i~FHMSqbpYy_L3O(MwGL3wNx~CQSPMB~u zB9v7^tAkbQwzMstIC0_^x)S6sTq3K~y8??A#uj)zf;+F$OFKBM)Baa@csQLh-A+D| zeN~5iCnj_W2xVyXt5>g1-&j|?(aIS|V13I)*@h1>BMQS+ zwe$S492@H$$F`jmV%ufMwr$(CZQHgz_`lTOsaH2nrfvrJR-M(`*Es#pf6%A6<(9na0X9wtD`8XH2P<^wY4pL^UXIuLj4I79oj;)BvePII@3j# z`3coDFKX~ibHr7QZdHsF5aI1tsfK8}exH*{rFyTq=9*iTYy?C+eTiT6-&n^A%4?7s8mBq_$Smy%R4Z-Hgl21k-3lu&q87y`U4Q=fAWBS@gtzGo z=o+5Oq}v{JY+YA}a6Jm1kqV+JC$VN}qFs8a-65O=`1$oa*@WcS3qNF05&9>;uv^n~7=PtYKvW04^-z00Tr!O&Sf7@jB;Y;3lcK-L~3cF2<86jNd0JIGk*(uC4||9pou=mFz9lsBl#1&qMtrMCW%^J#Ym1 zcI{y=jt(0d8rspRbz6wf=ZT+*m{{H$c?|;5Z1!jHi?q>q;KL6;{59EAJw#{`HcA|2 z(jZEQe0|`72Of&f(4Bt}8?4j(umDdC$;O;ylB8C5&`h5+X;KF?yylu~u71T8SKO@& zjs~#rb`k;7m<0|xfSF+(1t0gWyzJaF&X{ztqd`P-^gxV5HH^I6q;-2`sb)^yLXBoN$g3f{UXQ zbR!+jL|4|Co#kN;o*4Y1oOg+ITl3dYk=m6ye)~O#l?Tf|Hel4GXddFLH+I^XDa)a!KIW&;DK6CI!nvrf5nl!<|LMl>5u zuzhcYvZX|lWL+o~C-UTzPd@98JMMU!Y__hN0Yyk3VlWL?w&^$9^nreosBA$n7ro%` z?3GqpX>9|ii#gj|HCY!E64v==zEQ!^MKww+7K^ipJOU$0!3*!V-+p^5YI{)T?H5+k ztI7v>45nb_0M!2tn4be^I2i%k=!%?g82jFP@4Zh&Ma3e4ahM}>HE^n}gTg=ebrLoz zGgTK|2vs9#HX{Yo)-9QP?X}llSMT*a;kxUtdqO7g>2GBhD7@b2pt91t8I%0DjWx!3J=QM9&K#Ia3F8FdWE0>FJ&;gp(X?J^$FfzFKbxQ6 zbUGbai3RFyZ*N~tH?Hq=+;PX9sN>hv`M)f4_XP`!$s^(cSeO;S1*`n&V@Bcsa)6TW za=RDoqL&%1!x= zlC}V*z)Yopk}RdXVIiIOw;4|Go@`-@=;R6pm@U}L8U)81S&s=rYRrj5L{`uF?h7zCq( zUjZ=QhfbiBYxfHY2$Dq*DwC17fhw8%8fkW<>u;^}w$O%YH@%ntKG+B`gGs< z1T-TTOyj(0DLbb*Nx@7oJ^;%MQSQaBG;35xq{^RF`2S7`Kyh0|x1DoeDfc~Ylz=7y zR0LE=%FK{4OXMv1KaiXy>+hrkP$FPS05c{)6#-X7#ziFhITCGW%sqoSto+VsHI1DB O0000?3S7wcyyOM-n-02&+!#EIxIce+u1iBJ_{ZA{D_@u zcDyU!&S*>E@jvsdP}X7OsVqtzrcRKo6p#%(5zG1PuE3<= zA`eXEIYz+nMzu5Nv5#1B0mMV61gJgU$3uY&2C4%-{OZX9z2jO;A z)(PKVr@6=i#BjkurqDkA{#R>AS4A$iGyhO_NMg^dSD)p|oK4QcspI_YP>r^*N z3b&fROcF~|Hj+o*^CBOj#tYu%aH(1amHBswiR^;-Nj#3)SOY8fIc&ArMN=|p@yu{r_2BZ?Or-rRVji`=Ib%+v< z;*cH7+w_z*3JRvf)}LMwM*}~eM;M_Dq#SlTzto*^p{WCIe?7{ryVBlwfdhxa)=7nW z_XK)_SH#hvC@&0Npp{A5&lhr3C^V}$*ZTu_3%_A(ENcDLj}1bEJQJZ5#Y>0scHaj_ z_d6p(=sXaZ0mWy_xYbfvVoAkHI7DT#)#i#U8Pcn}n;RJjd9GQSVCwRm!MJa^ zA_PnC<^*qF(!}xP@UZPIW~F14JyswE)=-79U^+8sr|mSvspCQxIQKTFal^^c(I|rd zLS!qoj60ah&k)=oai)?l>@A|J2i6WRaBvmc?RGn-9?VH&oQ%=-^t@ZU+M8IznIi~t z$7){SyR*!vehy#>y|DhFJ7dLR&~gsYy_p=XFjrRgiMOnhc)vW;ApF&MeLzN4!_d&5 zrc#ihks9`JzcI%=>TrPBjSoCgB}H^bA|nlm)cL0qFzE6%^wrJCbNHjmVT^dQR9=JR zo^(OyDr%iBpkDySsCZwm1J1>CAI}rI8*M>}&SC+O2l%`cd#|u;OjzMGa9NG#dYcKx z6xwe%IXSIG%*Wtj1zLw(TKkuF1X60QRyPA)MJi!}#^r@S1$(IEain=Bd@C50GM!ah zqYHEQVAHsr8}34>{2P`#0Riz}#Y9`JjdROjf2uD%m+;Vnmx-o9KL-W|h|LFL|Dk@M zEoXy(a!X4~8(YuC@Gt^><1Pa#qeDaiYX;>o@3o)MJ6yC7d59#DM=tVD|I!1Ab9}C?WzDw zLS4nH(`DjN9BYiRjQQ+!P;*j^(bRBsRZCnhiPvy>WyO6(&ohG!Rn1S&Af)|W6)Ri1 zWk`FwK7dpMlwNM}>-I@~5%10lc@$!Dg6p_2zb}jdx`EBG|_Tz63 z2I5G&5m+~~nMJV0Q-uPlFa8cMrT9eD-F^TKO;92W5BXX0>>(c@natK}&@z{fWW2kZ zV_}}F*u3Jyp+|Mik$sgSuF(e?Ee+HfS#+H!atf0aHhPPi>=O3(_tP7NKJEl&icI`I z(YyS`ElU{_Y@+pMh*e`>{gj=KWWA9Kl|tA{T&K$5L5Bts=_dvGcE3Jqp)=;*oeLOz z0{u7~EW)&iUmUZXWl}t{4LHw0UWMW>n*LP&%kn-^ogyAm0Gua-A^cjhI01ywVT1*VjLa66 z>lhL(>KsKlqW&1q>}JXk_4o+Di3l_;UNP;{zR1-oI-0LG{@3K7Z%7?>nG5cHd?QcU zBruWY!m|t|iw*Q6!-Bu_x_S z$xV$7>3UNYKSaYs%={Nh#3vtwroS&?_XleOy}#$FAd$2yLu9UhFJVym$PJOCRNN?(%^x z_PU_MyiW6uNjo2lqyVDVyv=@t?KF!5+V`4WFt$qI3n!o7BPkd=Xu`2;%R2Q_feRWH z;d*o;BPR;mm(Pf}DzB(`45=n3fwEQ47@&nk50NAX0)F)MkznH?L?$-2o`Uy|95Q%l;Z&Shi0u6kR_+k~uFOB;k4QiZ0&2ct9P?V98Wg23Q>f|l z&oR!fW*M-9YEKZImyr5`IN{~PpS;`V%R<9sB$tQ~fi0bO;*6*xdvqtUyNV-c~TfqOcq^pV9zyLtX^G~dI(l3PDHt84ESjUJ|G zfmANRK5Z!L`UFea-m2Tqgu}}vW&zj&d3C2UOIOS$yRCSaS6BL&N-Td6@NHic6BFBD zKu39%Kto(A|D}oTt9n4gCCc%>0-EYzAy1${EnjHA;{6~&Ped{|*s z3AmAkoKsf;GETmA2^kg~RuWwYr>dpo*`Dch3o_6`BEOqTG4ara>QXIfaK{E^6~~Sx zLz!y6T+!(YKYP2*=Zv;lB*6~j0Eusx^idN3aol!oDBJ1>XoFvTW5^Enb?PRr*+7xb zwo+KT zd1y*@>p6kNH`}jdux3%5*F~^~7cKwop->)63Vj|HjRAoo<&Xd6{Z%2e4pd8bOfe1M zoPN04^Ve_uqJ^nzIZA^DtA+-?Sff0+-W<+0?_A0`fg`i#W!cYT3;<&dWMX||#N$-a z_xuwo02~K0X76L7;WjU|Y7yWuzJHBbfAmBcdR-bAcdVKw*B%{r9NA(W?yr3Sp%yBn zh5Ph?o9Ts8?!yIwJk3j094gfW)?1iNX^TLoLq-5f2-z8SHnm~7 z^70a)hEb=9q{kKr0)a>_2;0i7LAf4|KR*DmpH@A}-iR2qh^-GB(FmS5{^#Tuf9AeB zrLK25pp*r=!MnKr|JZ@0qGe+LWkdp@^+U3X2rhK~6E;f}QY*!+DrfO;++z-j&=%W-A+-HxhRhgG$H^Emm! z4zf3w3?qQSo(_a>K5>KH`ZCW0{I>UDY8QzD2}ltG#`1i0npH51ODeO(=0x+{&lmA6 z1+?Razxk?rCy8-0ts)i%X6Zpa7n6KF$^#IY2+l4F0P_i=Rsu?{`yx!*$LqbtB_%<} zQ>5@NAkx)NM!*jae_Rj28vY9M`)h;(@v5ofwI4(RevRD5>0{zy!+(%4q6OR{nfjZp z$Gq!`RJ_HddGOjtXYowu4bZ@R63N&4zEl5zX=EB;4Iv06aJ(wxk^{A=#);`su56hGGU~CC z{<2g<(R9@D7rQUh;&3hS^_iSASAzyN3ciPbu=j|)?e@MZ&GCax<)c6Z*2ZKsRGq9< zCNJi_V=tw2qVJc(I0CnnGrByA*HsMmua_Jfbn4`t_K1TLRSX-zZHP@r6sa3qAuy_W zR*8KSr`3*DWX{pK$mBVO-e3T|7tVqB!zQaQt@u5aL$Fxf5PgyMp45c?aTkSN0a@N@M0$2U6K5YX^S{pmG>7cjL z5DN-9;gjuBmiuOxdBMv!)&SY&q{&*55{EU}j&5m1P-aR>3Kvc58zQ9iUo?OO(YLa< zqiW%vuax+xH6qvmJGovjPAgxYts8dO3V8vJ9WOW&pnab@Ca4m3LgQE zK;O#+4I;dH(}W^4s1?iZc%uvyL_*mN@cUI=)~@p}=6+H)pB>Rl^sR-+hbXt^{C zXQLurlDOIJ_V^zwOxy{?rmqCBJqzT570blD<=x?LE7AddKWDIhZE`#Y%r*rCKK$eZ zp?iD+Y1mh0OI0pA<%w7kpi~PoJOenm73sc82c2EzOzkFY}IOC-PPOJJmaX zs&UFkIy7maC5jn;t;Vu%N@l@{FJGA%0Dj^J4_SbOxi?LOAH?pqcJIPrNphD%I2G8C z$QmYA^WD*`HP8qGGA5fX7x4R>3$qdTHBN{aYd0>{W^<3^-d@x~ZwQ}fE;{P<{i19d z3Iq@O9v-Qb5X*oc8Fi_EFfj1;WCK4D!*aX=5fj<`8(WvKrRRaGn--G#Oa+8PUMDaT4 zqeo-1$udL1fL393kmadMT3t3k7~}SBw`nj13e~r1P+<0sXa*nemkd_$x&Po8plg)t zOLUZW3}7FDE{>RI*YzjrWUK3*9B!mgPxWQa3Iojwwz%|!o}A4^MzL+mfaTQ6@l!X} zQN09Up+xHleD&rj=oE{Hb6v`W*i&Ek6F^inyPlXsAptDj@$>VvP@+X>wS4?7*Q>ss>|Ec^n{SFZ9LCHSbsc zV|K1?EPf6S+&AG~>8`FIj;CDSK05M!7YEvQ81d* zd>8_7adG+CFP+(m4cNF?C=hzwhkWMrH(%zS5mfB&Bu(`r5n6)0K$BxQZ*r=wYSf+J1R^UDD;mQ4Up z2~qT(14Dz$)_~=I=)?|0Yqp!mtWQnr?d=VZY-GBXho~WF+a(h@?EoKmatWpVmly`U zgLEVK@Gta8+g`|Dj3{OvC>&D{J7Zae|FpKf4ZG8uy4)El1N0dHS+JD!WdJX70(>lw z_xBAcC{%CF{}BMK+Q*U*wCc$v7`+73U!P9hUhuF!JS8DNbQ~xk8(jm`F0C{$uDUeJ zRNo@J!>W`Y9J|2!H@8Me?d3}Ru^Z#u@a(yALmIF15H$vKw9#Vzg~ ztQ5|7d;amyJNs9_4MHfqx*GlRdwqR9b)zrh6&>9bwIQ|<%yhl#dsgJ4+NGGmG1f}x z{7EzTI1FyWzBCSFiDaqBH8ND0oY`Ryb6C6PN^U2}>l*N_`}O5%9%5PrKqU?BZx2G_ zwf+0Or9q>_N?rZ0S`@&_mr8shOU_2BUU|UhnoL-@JcckyP*ikc70+WG8Uz?{6ftR$ z2tj(w*JA<3_6@opJnXQZQcML`ePmG*xMj(&Wd`=#IT zWwwGIS{b^sd5QaDHmwj#3|2!^cR()W(O`L;BTZ2#2V!&1w9f!T{5O(532H3^bKgnX zhw!LmahG6mj&xQkq-#OG{s4dcD6N%`>l-IBM}sxV3}mncEz%dn6;4UbPiIQl{W(f8 zZIuNqq-t1=(P=0tts=LCu}=Lbw8bE|i2|(Bv>+6Up+!Z*aIDMPN~qGeZ3` zh$}(@r!tC%R#Qk_9?o9&`Dc&%8lEfH(lR1PJ@)v|e*mGEJ3r1c-k%m^qU+PlmPU5 zi_cbQjmQ^UV(21~WACWu;Q+pdmdL{sp7omp=NM7sCM_@JWvNw4ufAf0Ue%YBpe0ydjsS?@(l(7+~E=ve%BACh{G{7uJR?^o9&77Puh52 z(Gx7I{9Aw0faq?T*Udio4;4gopk}{i)8!;3N~`(&7xe7pLP7NiY?O^9x?pySPFx`= zeg|^XbxWV?SX`M~kLba49f)2oCx46dceLrs#ykb{Ee=(Y2C~9Zt)aoEiaYT;Lc+~{ z&*Oy)IgYvXA6slxSYZ?jk%lM_7sbFfSM1sZSAe_v6U?fj6v#m0cqbY;6VFR>Zww;#q zWqqp#eE$hAXCO_5u8uI(4WbO63zj3~Rr|e8B+<%!YtdXoTc|W__pr3wQF&v5E%$GC zEN4C#eN#yHHB}jyhYP@oo{~=r{Y_;oUpEZO&7W$zIVDQ zz3A+GK=qd(SZ_8amas!OQ~eTd2r_o`$9@viX!AHZL|5XoU#MyO`e1I#`vcSscLZW7 z@5b|y{sTS~nL8*6uJT1T8A)sl+G3@cQjfjbqCG#~5XINNpSMN&wEuv0lNAuToF4WA zBV;+DXsKFV^%{YDN$`9}B`}eSlatf;dVi`jsQ(d;q1%)M@4NBe_YbGBMp(39 z%JJmm2{7b=tU>@yagIkEe}f{uhX48o|17+UnT;@bytYPf0=zX1Na63Ep@y&#JI+-S z&nfeNt~HZU5k<7u#pOkrbd(YBep)TvZm=evi!FOa^r)-jcVOZi%3J#Y{SG=yxU|S5 z)6uQL@96E*M4pTfi( z>|9ush4g2bAA+OEazQX|6cbZ7J-m-XQl}cS=|xxU-j9R7e-~c5%KALcr*P_i?%d{& z80VBjJnfJOiaqIjq?Hq(5JFnUhW;e zYWg3fwR&p*x_{0e;ap_}pj3-khRRtQuwtwy^8;Uwo6zL=n z&_u%s&txIhLGP;D|B3JWL2>P zlf_221v+Qk{UpHTL;XT2S)R?v(JRJ5pPj(L?pPjhKZtAfM&oWt7o)A zNqM$bg{Ez|BRS8#irbku;Fklgxo*Ty)EDZd>dCNu{JyTj;A_(x2ruI+Kngeg3rPP^ zKvYXUvSbU55=NkGrY!#eDOI=WiL%jxtfe^&KlN^5r!5)y zSF<|C{#eKIL=~7y<;nLIiP$40s_X&0gIs*@MWKAccScHE8QKp35*4q{OJJA=ra{>c zfxgnp(o?y!{riip17RmoqRgZk4Mh(BnNNONiIQaQEg!?n^D-6&xex|Plg)lTBFJ$t zBp$f})?7y{FS{KVQfHCGd}QIm*n8{oTrmg%Dv$e2hA4{ywHiUNOSx{X*?qXtZjP1q zT8m4F5(xz))E!*q$9GnOSal5Ex<5$zH?k8!_FRVz0q>{3ct=kA{|Z zfeG55aLz*|0u zaGXr@h4 zA3?u6Q_(++HEkOTM>Pt)kM}E1j_pY`0Ej*HPabsM{$VWeLspup)T=Lgfx9L?xu*Fv?ncng8 zk)Yt7(`6x#6zSe~g^~TWCaRP3xm??^^As9_u8F5#4<9~5<3){L2UmFkddc9atHq|y ztwskP=PrFXSNs1t#`V%QuhyzIWfxyT`$h0@dqhW1&qs|d$HH$z6BU=PCKrI9Ylw)W&czkeTg3QJ%wQN_t_+Gqo%CCvMg)?x1p1=SuZl zsQdO%((@GE4n3(Abu$O2T0xsy@rbumhUZ$Be}rnp481!`<{Hs!d!9I&x$^UHY$?+S z>SD9g8bB(ks+L3Ch3xAQDCExDl$Viz%_cgctr)Xh@lX@Z$Q*X|E;fwj6AraxC^|Hj z1N*z;3+zFW!>Ge60y9cdnGar7C#X*L&Nsb{~f~ zWR=BhDoLO`j$N^2BfKDZo9g&?eGG}=@X83^(UDrp_gh9_NAHvRajC`!CGorf;aS^U zagW%oaz%l^AgK>gcr(BB?~KWEgrWv}YFU?!`2Vf9%lQYYsHh-6-V0X2-n*&Cv=zmt zR{dpJ=`ZFGb4VEx2)f3U(cq!_b(9IAuXnztskQtrpY+wl=>DrVH?uT%!)mCcNVq$L!6g-0xy4R2JK-Q5{`I# zQ~xEA5$ImRBQkK`oy(BcM%5-x)6Z-@SuatpUczEB?{WcW8OE3AB5AUS0ok-f#e2A| zK{n3oX^vyUPUWXoh`e*q#Q+8aAQ1T{7c&ePW@7k}ujjCcZ8IL8Ln*B!;n|zNW z3u#Mn=cCy7eRcu+t-*m0A9@)r!RGhC_kNpx%2r5?tr#n9jm-MPnRTV3%)TWdp$d#@ z7^|}??5cEQ0j&+%5sqsyj{9AT@*JM{lp#rAT2!cW|Zno zx~W;Cp)<&_W&!~Zuc^!$>BYJMk#>gU!r{!<%o)p4r0H?_XFr5Qv+6aaUyTU_`2~Jfkm-%@yuMhsg zMMiL)iMC!4tGFjp6{p4Z+k!tf6N@Nk6p(WCy>N75ZeYCt>k|5!)&|TiwItbA*wpLz z+UlIN`wxDc5N`i-B)(y9XE|-io15ZPB3wz(euy4 zM=AonmQ$bT{d6pPc_HBrObA?l!{OYxee=!%enQJ!YGT{mGTqE19NF5GbZM~=%UKj6 zs0D$?CVChe0n0pmkv<>Vo%5SmS3u1B@^|n@KO5CsbZXX?*cFP6v?=}9@q9l{!S_Cr zxl3^AdJR72npbVKe-_I4>I?^S7uh#N7HIOoY!phn!zjWBp)~EZ^0M+p=k`eW!{fhO zKKC4tYr(fLpc!5$!83OP1|kDM%=RC1fBQ!>MiyqtcBvtAPxPlp&8?kau(N$CYksQD-YlPbh4vhd%Z z64NGbW_#Fl6DMic`pu(Q%Zx3eHZeuz6(qI=0V2o-HCJcQ$VB&EQkY#Ik|XYZ*Ws40 zvcP?JOxooV`XjoTAtgz5J;?%*1PLRwMtsQtFX9}!@hlk#yBJ)Htq?fnC!OFE=odUr z6u7NCS&i>5+JN!eT8Y=!WW7ye0(#)#368n!w1VN}?S&w~1P z+1Iq{P>SeDFu>^r9j3?mWt;NF-u}(h2F6;@*|%8HdTC#bUApgVsY>6%{rmXisru_C z%Y3xuON9`)YN2$UR+Btn3``#OD?Gj+D+j>#41)hf@f2T8;uKMY*DX*s&%zA}vm0rv zGYb7Hp-?4O&A)5x^F!*_Sdr-V`@0BN73^m5YD*l3dwn1mwI z+gQL;V2|m%-X`JHq+P&LvKXBA(Yx*pYf&H>5Hy&(PuB7pH=4g~Bao)X{`KVQ>6!8R z+LXSXn4H?yoH(jspF+&h4tikg{u8)K+l4E|=GzXu`QCI5oz7{zW)M-RzFy`@>FRB? zq|CYJ4P+lwbct3=_w-S(AmmK+*(i$Hw3X;UvhCXE{~ip)|dX+qill&JfM zx|ObMJAB`+oVn4xSHE}KE^hS==E1~C1G2=U00BkhMm7QUSbk;jk2-}0FYQ(Hej>>M zuHU)C(y~;njtvB)Z=MaGsqPKFCfX^TQaAd{I8G^Qp{i8d`gAvaPq3xD{mhknAQFNT znvd;07tPHoi^0uDZAF^w5hst2@aCEPI`SZ~SY{lFmt}_SZ-}xNeaGpwwZvo1U33o~ zyo^pI0V+!7?f^G3*PFx!uqh%z2+I$mAI4D&pw_S3${MT^C#-qr6dNJeXHD24tSH9# zgo$eXE*KxR=sqT1_Pi#N8nFK936t|kIHx*JVVB-`Bh_Cdz$0*H>K_@hEjkr)EH)~M nT_xa0rZ2p8v=`VMwgSqw1s!`SXB7Rn?en9IvUHWCN$~#w$6JiI literal 0 HcmV?d00001 diff --git a/demos/gvr/src/main/res/values/strings.xml b/demos/gvr/src/main/res/values/strings.xml new file mode 100644 index 0000000000..08feccb398 --- /dev/null +++ b/demos/gvr/src/main/res/values/strings.xml @@ -0,0 +1,28 @@ + + + + + ExoPlayer VR Demo + + Cleartext traffic not permitted + + Unrecognized stereo mode + + Media includes video tracks, but none are playable by this device + + Media includes audio tracks, but none are playable by this device + + diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java index 2c912c17f2..38fa3a36e5 100644 --- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java +++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java @@ -50,7 +50,10 @@ import com.google.vr.sdk.controller.ControllerManager; import javax.microedition.khronos.egl.EGLConfig; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** Base activity for VR 360 video playback. */ +/** + * Base activity for VR 360 video playback. Before starting the video playback a player needs to be + * set using {@link #setPlayer(Player)}. + */ public abstract class GvrPlayerActivity extends GvrActivity { private static final int EXIT_FROM_VR_REQUEST_CODE = 42; diff --git a/settings.gradle b/settings.gradle index d4530d67b7..50fdb68f30 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,10 +21,12 @@ if (gradle.ext.has('exoplayerModulePrefix')) { include modulePrefix + 'demo' include modulePrefix + 'demo-cast' include modulePrefix + 'demo-ima' +include modulePrefix + 'demo-gvr' include modulePrefix + 'playbacktests' project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main') project(modulePrefix + 'demo-cast').projectDir = new File(rootDir, 'demos/cast') project(modulePrefix + 'demo-ima').projectDir = new File(rootDir, 'demos/ima') +project(modulePrefix + 'demo-gvr').projectDir = new File(rootDir, 'demos/gvr') project(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests') apply from: 'core_settings.gradle' From 8bd2b5b3d794ac36972731ccac650b0a9d4d6961 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 5 Jun 2019 12:14:14 +0100 Subject: [PATCH 1346/1556] Fix detection of current window index in CastPlayer Issue:#5955 PiperOrigin-RevId: 251616118 --- RELEASENOTES.md | 2 ++ .../exoplayer2/ext/cast/CastPlayer.java | 23 +++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e03a0d2dc9..5128abba46 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -55,6 +55,8 @@ * Fix bug caused by parallel adaptive track selection using `Format`s without bitrate information ([#5971](https://github.com/google/ExoPlayer/issues/5971)). +* Fix bug in `CastPlayer.getCurrentWindowIndex()` + ([#5955](https://github.com/google/ExoPlayer/issues/5955)). ### 2.10.1 ### diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 8f15fb8789..db6f71286e 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -552,7 +552,17 @@ public final class CastPlayer extends BasePlayer { notificationsBatch.add( new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(this.repeatMode))); } - int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus()); + maybeUpdateTimelineAndNotify(); + + int currentWindowIndex = C.INDEX_UNSET; + MediaQueueItem currentItem = remoteMediaClient.getCurrentItem(); + if (currentItem != null) { + currentWindowIndex = currentTimeline.getIndexOfPeriod(currentItem.getItemId()); + } + if (currentWindowIndex == C.INDEX_UNSET) { + // The timeline is empty. Fall back to index 0, which is what ExoPlayer would do. + currentWindowIndex = 0; + } if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) { this.currentWindowIndex = currentWindowIndex; notificationsBatch.add( @@ -565,7 +575,6 @@ public final class CastPlayer extends BasePlayer { new ListenerNotificationTask( listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection))); } - maybeUpdateTimelineAndNotify(); flushNotifications(); } @@ -715,16 +724,6 @@ public final class CastPlayer extends BasePlayer { } } - /** - * Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If - * there is no media session, returns 0. - */ - private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) { - Integer currentItemId = mediaStatus != null - ? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null; - return currentItemId != null ? currentItemId : 0; - } - private static boolean isTrackActive(long id, long[] activeTrackIds) { for (long activeTrackId : activeTrackIds) { if (activeTrackId == id) { From 3490bea339e4b7c7177a70bf878050506acad794 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 5 Jun 2019 12:28:37 +0100 Subject: [PATCH 1347/1556] Simplify re-creation of the CastPlayer queue in the Cast demo app PiperOrigin-RevId: 251617354 --- .../exoplayer2/castdemo/DefaultReceiverPlayerManager.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java index a837bd77e5..bc38cbdb8a 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java @@ -69,7 +69,6 @@ import org.json.JSONObject; private final Listener listener; private final ConcatenatingMediaSource concatenatingMediaSource; - private boolean castMediaQueueCreationPending; private int currentItemIndex; private Player currentPlayer; @@ -271,9 +270,6 @@ import org.json.JSONObject; public void onTimelineChanged( Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { updateCurrentItemIndex(); - if (currentPlayer == castPlayer && timeline.isEmpty()) { - castMediaQueueCreationPending = true; - } } // CastPlayer.SessionAvailabilityListener implementation. @@ -335,7 +331,6 @@ import org.json.JSONObject; this.currentPlayer = currentPlayer; // Media queue management. - castMediaQueueCreationPending = currentPlayer == castPlayer; if (currentPlayer == exoPlayer) { exoPlayer.prepare(concatenatingMediaSource); } @@ -355,12 +350,11 @@ import org.json.JSONObject; */ private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { maybeSetCurrentItemAndNotify(itemIndex); - if (castMediaQueueCreationPending) { + if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) { MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; for (int i = 0; i < items.length; i++) { items[i] = buildMediaQueueItem(mediaQueue.get(i)); } - castMediaQueueCreationPending = false; castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); } else { currentPlayer.seekTo(itemIndex, positionMs); From cfa837df5c20a217de574f6d8d53ef161716f973 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 6 Jun 2019 00:42:40 +0100 Subject: [PATCH 1348/1556] Don't throw DecoderQueryException from getCodecMaxSize It's only thrown in an edge case on API level 20 and below. If it is thrown it causes playback failure when playback could succeed, by throwing up through configureCodec. It seems better just to catch the exception and have the codec be configured using the format's own width and height. PiperOrigin-RevId: 251745539 --- .../mediacodec/MediaCodecRenderer.java | 4 +-- .../video/MediaCodecVideoRenderer.java | 35 ++++++++++--------- .../testutil/DebugRenderersFactory.java | 4 +-- 3 files changed, 20 insertions(+), 23 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 d636467303..d00b218c38 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 @@ -455,15 +455,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption. * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if * no codec operating rate should be set. - * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ protected abstract void configureCodec( MediaCodecInfo codecInfo, MediaCodec codec, Format format, MediaCrypto crypto, - float codecOperatingRate) - throws DecoderQueryException; + float codecOperatingRate); protected final void maybeInitCodec() throws ExoPlaybackException { if (codec != null || inputFormat == null) { 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 33eb1095c3..45ab06db45 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 @@ -581,8 +581,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { MediaCodec codec, Format format, MediaCrypto crypto, - float codecOperatingRate) - throws DecoderQueryException { + float codecOperatingRate) { codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); MediaFormat mediaFormat = getMediaFormat( @@ -1210,11 +1209,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @param format The format for which the codec is being configured. * @param streamFormats The possible stream formats. * @return Suitable {@link CodecMaxValues}. - * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ protected CodecMaxValues getCodecMaxValues( - MediaCodecInfo codecInfo, Format format, Format[] streamFormats) - throws DecoderQueryException { + MediaCodecInfo codecInfo, Format format, Format[] streamFormats) { int maxWidth = format.width; int maxHeight = format.height; int maxInputSize = getMaxInputSize(codecInfo, format); @@ -1264,17 +1261,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } /** - * Returns a maximum video size to use when configuring a codec for {@code format} in a way - * that will allow possible adaptation to other compatible formats that are expected to have the - * same aspect ratio, but whose sizes are unknown. + * Returns a maximum video size to use when configuring a codec for {@code format} in a way that + * will allow possible adaptation to other compatible formats that are expected to have the same + * aspect ratio, but whose sizes are unknown. * * @param codecInfo Information about the {@link MediaCodec} being configured. * @param format The format for which the codec is being configured. * @return The maximum video size to use, or null if the size of {@code format} should be used. - * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ - private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) - throws DecoderQueryException { + private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) { boolean isVerticalVideo = format.height > format.width; int formatLongEdgePx = isVerticalVideo ? format.height : format.width; int formatShortEdgePx = isVerticalVideo ? format.width : format.height; @@ -1292,12 +1287,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return alignedSize; } } else { - // Conservatively assume the codec requires 16px width and height alignment. - longEdgePx = Util.ceilDivide(longEdgePx, 16) * 16; - shortEdgePx = Util.ceilDivide(shortEdgePx, 16) * 16; - if (longEdgePx * shortEdgePx <= MediaCodecUtil.maxH264DecodableFrameSize()) { - return new Point(isVerticalVideo ? shortEdgePx : longEdgePx, - isVerticalVideo ? longEdgePx : shortEdgePx); + try { + // Conservatively assume the codec requires 16px width and height alignment. + longEdgePx = Util.ceilDivide(longEdgePx, 16) * 16; + shortEdgePx = Util.ceilDivide(shortEdgePx, 16) * 16; + if (longEdgePx * shortEdgePx <= MediaCodecUtil.maxH264DecodableFrameSize()) { + return new Point( + isVerticalVideo ? shortEdgePx : longEdgePx, + isVerticalVideo ? longEdgePx : shortEdgePx); + } + } catch (DecoderQueryException e) { + // We tried our best. Give up! + return null; } } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index e1243d34ba..d6b72048a1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -31,7 +31,6 @@ import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; -import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.nio.ByteBuffer; @@ -115,8 +114,7 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { MediaCodec codec, Format format, MediaCrypto crypto, - float operatingRate) - throws DecoderQueryException { + float operatingRate) { // If the codec is being initialized whilst the renderer is started, default behavior is to // render the first frame (i.e. the keyframe before the current position), then drop frames up // to the current playback position. For test runs that place a maximum limit on the number of From 624bb6b8d1709852e8cffc5c67eb7d5debd9e848 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 6 Jun 2019 01:00:22 +0100 Subject: [PATCH 1349/1556] Attach timestamp to ExoPlaybackException PiperOrigin-RevId: 251748542 --- .../com/google/android/exoplayer2/ExoPlaybackException.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java index b5f8f954bb..49aacd9638 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.os.SystemClock; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.MediaSource; @@ -73,6 +74,9 @@ public final class ExoPlaybackException extends Exception { */ public final int rendererIndex; + /** The value of {@link SystemClock#elapsedRealtime()} when this exception was created. */ + public final long timestampMs; + @Nullable private final Throwable cause; /** @@ -131,6 +135,7 @@ public final class ExoPlaybackException extends Exception { this.type = type; this.cause = cause; this.rendererIndex = rendererIndex; + timestampMs = SystemClock.elapsedRealtime(); } private ExoPlaybackException(@Type int type, String message) { @@ -138,6 +143,7 @@ public final class ExoPlaybackException extends Exception { this.type = type; rendererIndex = C.INDEX_UNSET; cause = null; + timestampMs = SystemClock.elapsedRealtime(); } /** From 5b02f92dad9c5725ed32e67f326b1499ca3e5dde Mon Sep 17 00:00:00 2001 From: sr1990 Date: Mon, 10 Jun 2019 22:15:04 -0700 Subject: [PATCH 1350/1556] [Patch V2] Support signalling of last segment number via supplemental descriptor in mpd --- .../source/dash/DefaultDashChunkSource.java | 28 +------ .../dash/manifest/DashManifestParser.java | 35 ++++++--- .../source/dash/manifest/SegmentBase.java | 74 ++++++++++++++++++- 3 files changed, 100 insertions(+), 37 deletions(-) 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 2877b2a1cc..02b2990193 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 @@ -42,7 +42,6 @@ import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk; import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler; 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.Descriptor; import com.google.android.exoplayer2.source.dash.manifest.RangedUri; import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -326,35 +325,10 @@ public class DefaultDashChunkSource implements DashChunkSource { return; } - List listDescriptors; - Integer lastSegmentNumberSchemeIdUri = Integer.MAX_VALUE; - String sampleMimeType = trackSelection.getFormat(periodIndex).sampleMimeType; - - if (sampleMimeType.contains("video") || sampleMimeType.contains("audio")) { - - int track_type = sampleMimeType.contains("video")? C.TRACK_TYPE_VIDEO : C.TRACK_TYPE_AUDIO; - - if (!manifest.getPeriod(periodIndex).adaptationSets.get(manifest.getPeriod(periodIndex) - .getAdaptationSetIndex(track_type)).supplementalProperties.isEmpty()) { - listDescriptors = manifest.getPeriod(periodIndex).adaptationSets - .get(manifest.getPeriod(periodIndex).getAdaptationSetIndex(track_type)) - .supplementalProperties; - for ( Descriptor descriptor: listDescriptors ) { - if (descriptor.schemeIdUri.equalsIgnoreCase - ("http://dashif.org/guidelines/last-segment-number")) { - lastSegmentNumberSchemeIdUri = Integer.valueOf(descriptor.value); - } - } - } - } - long firstAvailableSegmentNum = representationHolder.getFirstAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs); long lastAvailableSegmentNum = - Math.min(representationHolder. - getLastAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs), - lastSegmentNumberSchemeIdUri); - + representationHolder.getLastAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs); updateLiveEdgeTimeUs(representationHolder, lastAvailableSegmentNum); long segmentNum = 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 64ec1adb43..37230696f8 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 @@ -242,7 +242,7 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { segmentBase = parseSegmentList(xpp, null); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { - segmentBase = parseSegmentTemplate(xpp, null); + segmentBase = parseSegmentTemplate(xpp, null,null); } else { maybeSkipTag(xpp); } @@ -323,7 +323,8 @@ public class DashManifestParser extends DefaultHandler language, roleDescriptors, accessibilityDescriptors, - segmentBase); + segmentBase, + supplementalProperties); contentType = checkContentTypeConsistency(contentType, getContentType(representationInfo.format)); representationInfos.add(representationInfo); @@ -332,7 +333,7 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { - segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase); + segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase,supplementalProperties); } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); } else if (XmlPullParserUtil.isStartTag(xpp)) { @@ -485,7 +486,8 @@ public class DashManifestParser extends DefaultHandler String adaptationSetLanguage, List adaptationSetRoleDescriptors, List adaptationSetAccessibilityDescriptors, - SegmentBase segmentBase) + SegmentBase segmentBase, + ArrayList parentSupplementalProperties) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE); @@ -517,7 +519,8 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { - segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase); + segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase, + parentSupplementalProperties); } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { Pair contentProtection = parseContentProtection(xpp); if (contentProtection.first != null) { @@ -756,7 +759,8 @@ public class DashManifestParser extends DefaultHandler startNumber, duration, timeline, segments); } - protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, SegmentTemplate parent) + protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, SegmentTemplate parent, + ArrayList parentSupplementalProperties) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", @@ -788,7 +792,8 @@ public class DashManifestParser extends DefaultHandler } return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, - startNumber, duration, timeline, initializationTemplate, mediaTemplate); + startNumber, duration, timeline, initializationTemplate, mediaTemplate, + parentSupplementalProperties); } protected SegmentTemplate buildSegmentTemplate( @@ -799,9 +804,21 @@ public class DashManifestParser extends DefaultHandler long duration, List timeline, UrlTemplate initializationTemplate, - UrlTemplate mediaTemplate) { + UrlTemplate mediaTemplate,ArrayList supplementalProperties ) { + + if (supplementalProperties != null) { + for (Descriptor descriptor : supplementalProperties) { + if (descriptor.schemeIdUri.equalsIgnoreCase + ("http://dashif.org/guidelines/last-segment-number")) { + return new SegmentTemplate(initialization, timescale, presentationTimeOffset, + startNumber, Integer.valueOf(descriptor.value),duration, timeline, + initializationTemplate, mediaTemplate); + } + } + } + return new SegmentTemplate(initialization, timescale, presentationTimeOffset, - startNumber, duration, timeline, initializationTemplate, mediaTemplate); + startNumber,duration, timeline, initializationTemplate, mediaTemplate); } /** diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java index f033232590..720c20eed2 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java @@ -102,6 +102,7 @@ public abstract class SegmentBase { /* package */ final long startNumber; /* package */ final long duration; /* package */ final List segmentTimeline; + /* package */ final int endNumber; /** * @param initialization A {@link RangedUri} corresponding to initialization data, if such data @@ -128,6 +129,38 @@ public abstract class SegmentBase { this.startNumber = startNumber; this.duration = duration; this.segmentTimeline = segmentTimeline; + this.endNumber = C.INDEX_UNSET; + } + + /** + * @param initialization A {@link RangedUri} corresponding to initialization data, if such data + * exists. + * @param timescale The timescale in units per second. + * @param presentationTimeOffset The presentation time offset. The value in seconds is the + * division of this value and {@code timescale}. + * @param startNumber The sequence number of the first segment. + * @param endNumber The sequence number of the last segment specified by SupplementalProperty + * schemeIdUri="http://dashif.org/guidelines/last-segment-number" + * @param duration The duration of each segment in the case of fixed duration segments. The + * value in seconds is the division of this value and {@code timescale}. If {@code + * segmentTimeline} is non-null then this parameter is ignored. + * @param segmentTimeline A segment timeline corresponding to the segments. If null, then + * segments are assumed to be of fixed duration as specified by the {@code duration} + * parameter. + */ + public MultiSegmentBase( + RangedUri initialization, + long timescale, + long presentationTimeOffset, + long startNumber, + int endNumber, + long duration, + List segmentTimeline) { + super(initialization, timescale, presentationTimeOffset); + this.startNumber = startNumber; + this.duration = duration; + this.segmentTimeline = segmentTimeline; + this.endNumber = endNumber; } /** @see DashSegmentIndex#getSegmentNum(long, long) */ @@ -312,6 +345,43 @@ public abstract class SegmentBase { this.mediaTemplate = mediaTemplate; } + /** + * @param initialization A {@link RangedUri} corresponding to initialization data, if such data + * exists. The value of this parameter is ignored if {@code initializationTemplate} is + * non-null. + * @param timescale The timescale in units per second. + * @param presentationTimeOffset The presentation time offset. The value in seconds is the + * division of this value and {@code timescale}. + * @param startNumber The sequence number of the first segment. + * @param endNumber The sequence number of the last segment specified by SupplementalProperty + * schemeIdUri="http://dashif.org/guidelines/last-segment-number" + * @param duration The duration of each segment in the case of fixed duration segments. The + * value in seconds is the division of this value and {@code timescale}. If {@code + * segmentTimeline} is non-null then this parameter is ignored. + * @param segmentTimeline A segment timeline corresponding to the segments. If null, then + * segments are assumed to be of fixed duration as specified by the {@code duration} + * parameter. + * @param initializationTemplate A template defining the location of initialization data, if + * such data exists. If non-null then the {@code initialization} parameter is ignored. If + * null then {@code initialization} will be used. + * @param mediaTemplate A template defining the location of each media segment. + */ + public SegmentTemplate( + RangedUri initialization, + long timescale, + long presentationTimeOffset, + long startNumber, + int endNumber, + long duration, + List segmentTimeline, + UrlTemplate initializationTemplate, + UrlTemplate mediaTemplate) { + super(initialization, timescale, presentationTimeOffset, startNumber,endNumber, + duration, segmentTimeline); + this.initializationTemplate = initializationTemplate; + this.mediaTemplate = mediaTemplate; + } + @Override public RangedUri getInitialization(Representation representation) { if (initializationTemplate != null) { @@ -338,7 +408,9 @@ public abstract class SegmentBase { @Override public int getSegmentCount(long periodDurationUs) { - if (segmentTimeline != null) { + if( endNumber != C.INDEX_UNSET) { + return endNumber; + } else if (segmentTimeline != null) { return segmentTimeline.size(); } else if (periodDurationUs != C.TIME_UNSET) { long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; From 28ee05f657c2051f6128fb0e35c859431570a5fd Mon Sep 17 00:00:00 2001 From: arodriguez Date: Fri, 14 Jun 2019 08:24:31 +0200 Subject: [PATCH 1351/1556] Support for UDP data source --- .../exoplayer2/upstream/DefaultDataSource.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java index bfc9a37844..aeaa977b12 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -55,6 +55,7 @@ public final class DefaultDataSource implements DataSource { private static final String SCHEME_ASSET = "asset"; private static final String SCHEME_CONTENT = "content"; private static final String SCHEME_RTMP = "rtmp"; + private static final String SCHEME_UDP = "udp"; private static final String SCHEME_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME; private final Context context; @@ -66,6 +67,7 @@ public final class DefaultDataSource implements DataSource { @Nullable private DataSource assetDataSource; @Nullable private DataSource contentDataSource; @Nullable private DataSource rtmpDataSource; + @Nullable private DataSource udpDataSource; @Nullable private DataSource dataSchemeDataSource; @Nullable private DataSource rawResourceDataSource; @@ -139,6 +141,7 @@ public final class DefaultDataSource implements DataSource { maybeAddListenerToDataSource(assetDataSource, transferListener); maybeAddListenerToDataSource(contentDataSource, transferListener); maybeAddListenerToDataSource(rtmpDataSource, transferListener); + maybeAddListenerToDataSource(udpDataSource, transferListener); maybeAddListenerToDataSource(dataSchemeDataSource, transferListener); maybeAddListenerToDataSource(rawResourceDataSource, transferListener); } @@ -161,6 +164,8 @@ public final class DefaultDataSource implements DataSource { dataSource = getContentDataSource(); } else if (SCHEME_RTMP.equals(scheme)) { dataSource = getRtmpDataSource(); + } else if(SCHEME_UDP.equals(scheme)){ + dataSource = getUdpDataSource(); } else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) { dataSource = getDataSchemeDataSource(); } else if (SCHEME_RAW.equals(scheme)) { @@ -199,6 +204,14 @@ public final class DefaultDataSource implements DataSource { } } + private DataSource getUdpDataSource(){ + if (udpDataSource == null) { + udpDataSource = new UdpDataSource(); + addListenersToDataSource(udpDataSource); + } + return udpDataSource; + } + private DataSource getFileDataSource() { if (fileDataSource == null) { fileDataSource = new FileDataSource(); From 1fb105bbb2bef0f543eb5e2bd909dddef623d420 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 6 Jun 2019 01:00:22 +0100 Subject: [PATCH 1352/1556] Attach timestamp to ExoPlaybackException PiperOrigin-RevId: 251748542 --- .../com/google/android/exoplayer2/ExoPlaybackException.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java index b5f8f954bb..49aacd9638 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.os.SystemClock; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.MediaSource; @@ -73,6 +74,9 @@ public final class ExoPlaybackException extends Exception { */ public final int rendererIndex; + /** The value of {@link SystemClock#elapsedRealtime()} when this exception was created. */ + public final long timestampMs; + @Nullable private final Throwable cause; /** @@ -131,6 +135,7 @@ public final class ExoPlaybackException extends Exception { this.type = type; this.cause = cause; this.rendererIndex = rendererIndex; + timestampMs = SystemClock.elapsedRealtime(); } private ExoPlaybackException(@Type int type, String message) { @@ -138,6 +143,7 @@ public final class ExoPlaybackException extends Exception { this.type = type; rendererIndex = C.INDEX_UNSET; cause = null; + timestampMs = SystemClock.elapsedRealtime(); } /** From e525c1c59ea0415d9c8c912c613cf363272e89fe Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 6 Jun 2019 21:31:11 +0100 Subject: [PATCH 1353/1556] Add CronetDataSource.read(ByteBuffer) method that writes directly into caller's buffer. PiperOrigin-RevId: 251915459 --- .../ext/cronet/CronetDataSource.java | 189 ++++++++-- .../ext/cronet/CronetDataSourceTest.java | 328 ++++++++++++++++++ 2 files changed, 492 insertions(+), 25 deletions(-) diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index a1ee80767d..7e30d924a0 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -40,6 +40,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.concurrent.Executor; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -504,32 +505,9 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { // Fill readBuffer with more data from Cronet. operation.close(); readBuffer.clear(); - castNonNull(currentUrlRequest).read(readBuffer); - try { - if (!operation.block(readTimeoutMs)) { - throw new SocketTimeoutException(); - } - } catch (InterruptedException e) { - // The operation is ongoing so replace readBuffer to avoid it being written to by this - // operation during a subsequent request. - this.readBuffer = null; - Thread.currentThread().interrupt(); - throw new HttpDataSourceException( - new InterruptedIOException(e), - castNonNull(currentDataSpec), - HttpDataSourceException.TYPE_READ); - } catch (SocketTimeoutException e) { - // The operation is ongoing so replace readBuffer to avoid it being written to by this - // operation during a subsequent request. - this.readBuffer = null; - throw new HttpDataSourceException( - e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ); - } + readInternal(castNonNull(readBuffer)); - if (exception != null) { - throw new HttpDataSourceException( - exception, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ); - } else if (finished) { + if (finished) { bytesRemaining = 0; return C.RESULT_END_OF_INPUT; } else { @@ -554,6 +532,115 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { return bytesRead; } + /** + * Reads up to {@code buffer.remaining()} bytes of data and stores them into {@code buffer}, + * starting at {@code buffer.position()}. Advances the position of the buffer by the number of + * bytes read and returns this length. + * + *

    If there is an error, a {@link HttpDataSourceException} is thrown and the contents of {@code + * buffer} should be ignored. If the exception has error code {@code + * HttpDataSourceException.TYPE_READ}, note that Cronet may continue writing into {@code buffer} + * after the method has returned. Thus the caller should not attempt to reuse the buffer. + * + *

    If {@code buffer.remaining()} is zero then 0 is returned. Otherwise, if no data is available + * because the end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is + * returned. Otherwise, the call will block until at least one byte of data has been read and the + * number of bytes read is returned. + * + *

    Passed buffer must be direct ByteBuffer. If you have a non-direct ByteBuffer, consider the + * alternative read method with its backed array. + * + * @param buffer The ByteBuffer into which the read data should be stored. Must be a direct + * ByteBuffer. + * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if no data is available + * because the end of the opened range has been reached. + * @throws HttpDataSourceException If an error occurs reading from the source. + * @throws IllegalArgumentException If {@codes buffer} is not a direct ByteBuffer. + */ + public int read(ByteBuffer buffer) throws HttpDataSourceException { + Assertions.checkState(opened); + + if (!buffer.isDirect()) { + throw new IllegalArgumentException("Passed buffer is not a direct ByteBuffer"); + } + if (!buffer.hasRemaining()) { + return 0; + } else if (bytesRemaining == 0) { + return C.RESULT_END_OF_INPUT; + } + int readLength = buffer.remaining(); + + if (readBuffer != null) { + // Skip all the bytes we can from readBuffer if there are still bytes to skip. + if (bytesToSkip != 0) { + if (bytesToSkip >= readBuffer.remaining()) { + bytesToSkip -= readBuffer.remaining(); + readBuffer.position(readBuffer.limit()); + } else { + readBuffer.position(readBuffer.position() + (int) bytesToSkip); + bytesToSkip = 0; + } + } + + // If there is existing data in the readBuffer, read as much as possible. Return if any read. + int copyBytes = copyByteBuffer(/* src= */ readBuffer, /* dst= */ buffer); + if (copyBytes != 0) { + if (bytesRemaining != C.LENGTH_UNSET) { + bytesRemaining -= copyBytes; + } + bytesTransferred(copyBytes); + return copyBytes; + } + } + + boolean readMore = true; + while (readMore) { + // If bytesToSkip > 0, read into intermediate buffer that we can discard instead of caller's + // buffer. If we do not need to skip bytes, we may write to buffer directly. + final boolean useCallerBuffer = bytesToSkip == 0; + + operation.close(); + + if (!useCallerBuffer) { + if (readBuffer == null) { + readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES); + } else { + readBuffer.clear(); + } + if (bytesToSkip < READ_BUFFER_SIZE_BYTES) { + readBuffer.limit((int) bytesToSkip); + } + } + + // Fill buffer with more data from Cronet. + readInternal(useCallerBuffer ? buffer : castNonNull(readBuffer)); + + if (finished) { + bytesRemaining = 0; + return C.RESULT_END_OF_INPUT; + } else { + // The operation didn't time out, fail or finish, and therefore data must have been read. + Assertions.checkState( + useCallerBuffer + ? readLength > buffer.remaining() + : castNonNull(readBuffer).position() > 0); + // If we meant to skip bytes, subtract what was left and repeat, otherwise, continue. + if (useCallerBuffer) { + readMore = false; + } else { + bytesToSkip -= castNonNull(readBuffer).position(); + } + } + } + + final int bytesRead = readLength - buffer.remaining(); + if (bytesRemaining != C.LENGTH_UNSET) { + bytesRemaining -= bytesRead; + } + bytesTransferred(bytesRead); + return bytesRead; + } + @Override public synchronized void close() { if (currentUrlRequest != null) { @@ -655,6 +742,47 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs; } + /** + * Reads up to {@code buffer.remaining()} bytes of data from {@code currentUrlRequest} and stores + * them into {@code buffer}. If there is an error and {@code buffer == readBuffer}, then it resets + * the current {@code readBuffer} object so that it is not reused in the future. + * + * @param buffer The ByteBuffer into which the read data is stored. Must be a direct ByteBuffer. + * @throws HttpDataSourceException If an error occurs reading from the source. + */ + private void readInternal(ByteBuffer buffer) throws HttpDataSourceException { + castNonNull(currentUrlRequest).read(buffer); + try { + if (!operation.block(readTimeoutMs)) { + throw new SocketTimeoutException(); + } + } catch (InterruptedException e) { + // The operation is ongoing so replace buffer to avoid it being written to by this + // operation during a subsequent request. + if (Objects.equals(buffer, readBuffer)) { + readBuffer = null; + } + Thread.currentThread().interrupt(); + throw new HttpDataSourceException( + new InterruptedIOException(e), + castNonNull(currentDataSpec), + HttpDataSourceException.TYPE_READ); + } catch (SocketTimeoutException e) { + // The operation is ongoing so replace buffer to avoid it being written to by this + // operation during a subsequent request. + if (Objects.equals(buffer, readBuffer)) { + readBuffer = null; + } + throw new HttpDataSourceException( + e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ); + } + + if (exception != null) { + throw new HttpDataSourceException( + exception, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ); + } + } + private static boolean isCompressed(UrlResponseInfo info) { for (Map.Entry entry : info.getAllHeadersAsList()) { if (entry.getKey().equalsIgnoreCase("Content-Encoding")) { @@ -738,6 +866,17 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { return list == null || list.isEmpty(); } + // Copy as much as possible from the src buffer into dst buffer. + // Returns the number of bytes copied. + private static int copyByteBuffer(ByteBuffer src, ByteBuffer dst) { + int remaining = Math.min(src.remaining(), dst.remaining()); + int limit = src.limit(); + src.limit(src.position() + remaining); + dst.put(src); + src.limit(limit); + return remaining; + } + private final class UrlRequestCallback extends UrlRequest.Callback { @Override diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index a01c5e84b6..2be369bad9 100644 --- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -554,6 +554,260 @@ public final class CronetDataSourceTest { assertThat(bytesRead).isEqualTo(16); } + @Test + public void testRequestReadByteBufferTwice() throws HttpDataSourceException { + mockResponseStartSuccess(); + mockReadSuccess(0, 16); + + dataSourceUnderTest.open(testDataSpec); + + ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8); + int bytesRead = dataSourceUnderTest.read(returnedBuffer); + assertThat(bytesRead).isEqualTo(8); + returnedBuffer.flip(); + assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8)); + + // Use a wrapped ByteBuffer instead of direct for coverage. + returnedBuffer.rewind(); + bytesRead = dataSourceUnderTest.read(returnedBuffer); + returnedBuffer.flip(); + assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(8, 8)); + assertThat(bytesRead).isEqualTo(8); + + // Separate cronet calls for each read. + verify(mockUrlRequest, times(2)).read(any(ByteBuffer.class)); + verify(mockTransferListener, times(2)) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8); + } + + @Test + public void testRequestIntermixRead() throws HttpDataSourceException { + mockResponseStartSuccess(); + // Chunking reads into parts 6, 7, 8, 9. + mockReadSuccess(0, 30); + + dataSourceUnderTest.open(testDataSpec); + + ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(6); + int bytesRead = dataSourceUnderTest.read(returnedBuffer); + returnedBuffer.flip(); + assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 6)); + assertThat(bytesRead).isEqualTo(6); + + byte[] returnedBytes = new byte[7]; + bytesRead += dataSourceUnderTest.read(returnedBytes, 0, 7); + assertThat(returnedBytes).isEqualTo(buildTestDataArray(6, 7)); + assertThat(bytesRead).isEqualTo(6 + 7); + + returnedBuffer = ByteBuffer.allocateDirect(8); + bytesRead += dataSourceUnderTest.read(returnedBuffer); + returnedBuffer.flip(); + assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(13, 8)); + assertThat(bytesRead).isEqualTo(6 + 7 + 8); + + returnedBytes = new byte[9]; + bytesRead += dataSourceUnderTest.read(returnedBytes, 0, 9); + assertThat(returnedBytes).isEqualTo(buildTestDataArray(21, 9)); + assertThat(bytesRead).isEqualTo(6 + 7 + 8 + 9); + + // First ByteBuffer call. The first byte[] call populates enough bytes for the rest. + verify(mockUrlRequest, times(2)).read(any(ByteBuffer.class)); + verify(mockTransferListener, times(1)) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6); + verify(mockTransferListener, times(1)) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 7); + verify(mockTransferListener, times(1)) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8); + verify(mockTransferListener, times(1)) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 9); + } + + @Test + public void testSecondRequestNoContentLengthReadByteBuffer() throws HttpDataSourceException { + mockResponseStartSuccess(); + testResponseHeader.put("Content-Length", Long.toString(1L)); + mockReadSuccess(0, 16); + + // First request. + dataSourceUnderTest.open(testDataSpec); + ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8); + dataSourceUnderTest.read(returnedBuffer); + dataSourceUnderTest.close(); + + testResponseHeader.remove("Content-Length"); + mockReadSuccess(0, 16); + + // Second request. + dataSourceUnderTest.open(testDataSpec); + returnedBuffer = ByteBuffer.allocateDirect(16); + returnedBuffer.limit(10); + int bytesRead = dataSourceUnderTest.read(returnedBuffer); + assertThat(bytesRead).isEqualTo(10); + returnedBuffer.limit(returnedBuffer.capacity()); + bytesRead = dataSourceUnderTest.read(returnedBuffer); + assertThat(bytesRead).isEqualTo(6); + returnedBuffer.rewind(); + bytesRead = dataSourceUnderTest.read(returnedBuffer); + assertThat(bytesRead).isEqualTo(C.RESULT_END_OF_INPUT); + } + + @Test + public void testRangeRequestWith206ResponseReadByteBuffer() throws HttpDataSourceException { + mockResponseStartSuccess(); + mockReadSuccess(1000, 5000); + testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests. + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); + + dataSourceUnderTest.open(testDataSpec); + + ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16); + int bytesRead = dataSourceUnderTest.read(returnedBuffer); + assertThat(bytesRead).isEqualTo(16); + returnedBuffer.flip(); + assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(1000, 16)); + verify(mockTransferListener) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16); + } + + @Test + public void testRangeRequestWith200ResponseReadByteBuffer() throws HttpDataSourceException { + // Tests for skipping bytes. + mockResponseStartSuccess(); + mockReadSuccess(0, 7000); + testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests. + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); + + dataSourceUnderTest.open(testDataSpec); + + ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16); + int bytesRead = dataSourceUnderTest.read(returnedBuffer); + assertThat(bytesRead).isEqualTo(16); + returnedBuffer.flip(); + assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(1000, 16)); + verify(mockTransferListener) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16); + } + + @Test + public void testReadByteBufferWithUnsetLength() throws HttpDataSourceException { + testResponseHeader.remove("Content-Length"); + mockResponseStartSuccess(); + mockReadSuccess(0, 16); + + dataSourceUnderTest.open(testDataSpec); + + ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16); + returnedBuffer.limit(8); + int bytesRead = dataSourceUnderTest.read(returnedBuffer); + returnedBuffer.flip(); + assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8)); + assertThat(bytesRead).isEqualTo(8); + verify(mockTransferListener) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8); + } + + @Test + public void testReadByteBufferReturnsWhatItCan() throws HttpDataSourceException { + mockResponseStartSuccess(); + mockReadSuccess(0, 16); + + dataSourceUnderTest.open(testDataSpec); + + ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(24); + int bytesRead = dataSourceUnderTest.read(returnedBuffer); + returnedBuffer.flip(); + assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 16)); + assertThat(bytesRead).isEqualTo(16); + verify(mockTransferListener) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16); + } + + @Test + public void testOverreadByteBuffer() throws HttpDataSourceException { + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16, null); + testResponseHeader.put("Content-Length", Long.toString(16L)); + mockResponseStartSuccess(); + mockReadSuccess(0, 16); + + dataSourceUnderTest.open(testDataSpec); + + ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8); + int bytesRead = dataSourceUnderTest.read(returnedBuffer); + assertThat(bytesRead).isEqualTo(8); + returnedBuffer.flip(); + assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8)); + + // The current buffer is kept if not completely consumed by DataSource reader. + returnedBuffer = ByteBuffer.allocateDirect(6); + bytesRead += dataSourceUnderTest.read(returnedBuffer); + assertThat(bytesRead).isEqualTo(14); + returnedBuffer.flip(); + assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(8, 6)); + + // 2 bytes left at this point. + returnedBuffer = ByteBuffer.allocateDirect(8); + bytesRead += dataSourceUnderTest.read(returnedBuffer); + assertThat(bytesRead).isEqualTo(16); + returnedBuffer.flip(); + assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(14, 2)); + + // Called on each. + verify(mockUrlRequest, times(3)).read(any(ByteBuffer.class)); + verify(mockTransferListener, times(1)) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8); + verify(mockTransferListener, times(1)) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6); + verify(mockTransferListener, times(1)) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 2); + + // Now we already returned the 16 bytes initially asked. + // Try to read again even though all requested 16 bytes are already returned. + // Return C.RESULT_END_OF_INPUT + returnedBuffer = ByteBuffer.allocateDirect(16); + int bytesOverRead = dataSourceUnderTest.read(returnedBuffer); + assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT); + assertThat(returnedBuffer.position()).isEqualTo(0); + // C.RESULT_END_OF_INPUT should not be reported though the TransferListener. + verify(mockTransferListener, never()) + .onBytesTransferred( + dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, C.RESULT_END_OF_INPUT); + // Number of calls to cronet should not have increased. + verify(mockUrlRequest, times(3)).read(any(ByteBuffer.class)); + // Check for connection not automatically closed. + verify(mockUrlRequest, never()).cancel(); + assertThat(bytesRead).isEqualTo(16); + } + + @Test + public void testClosedMeansClosedReadByteBuffer() throws HttpDataSourceException { + mockResponseStartSuccess(); + mockReadSuccess(0, 16); + + int bytesRead = 0; + dataSourceUnderTest.open(testDataSpec); + + ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16); + returnedBuffer.limit(8); + bytesRead += dataSourceUnderTest.read(returnedBuffer); + returnedBuffer.flip(); + assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8)); + assertThat(bytesRead).isEqualTo(8); + + dataSourceUnderTest.close(); + verify(mockTransferListener) + .onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true); + + try { + bytesRead += dataSourceUnderTest.read(returnedBuffer); + fail(); + } catch (IllegalStateException e) { + // Expected. + } + + // 16 bytes were attempted but only 8 should have been successfully read. + assertThat(bytesRead).isEqualTo(8); + } + @Test public void testConnectTimeout() throws InterruptedException { long startTimeMs = SystemClock.elapsedRealtime(); @@ -855,6 +1109,36 @@ public final class CronetDataSourceTest { } } + @Test + public void testReadByteBufferFailure() throws HttpDataSourceException { + mockResponseStartSuccess(); + mockReadFailure(); + + dataSourceUnderTest.open(testDataSpec); + ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8); + try { + dataSourceUnderTest.read(returnedBuffer); + fail("dataSourceUnderTest.read() returned, but IOException expected"); + } catch (IOException e) { + // Expected. + } + } + + @Test + public void testReadNonDirectedByteBufferFailure() throws HttpDataSourceException { + mockResponseStartSuccess(); + mockReadFailure(); + + dataSourceUnderTest.open(testDataSpec); + byte[] returnedBuffer = new byte[8]; + try { + dataSourceUnderTest.read(ByteBuffer.wrap(returnedBuffer)); + fail("dataSourceUnderTest.read() returned, but IllegalArgumentException expected"); + } catch (IllegalArgumentException e) { + // Expected. + } + } + @Test public void testReadInterrupted() throws HttpDataSourceException, InterruptedException { mockResponseStartSuccess(); @@ -886,6 +1170,37 @@ public final class CronetDataSourceTest { timedOutLatch.await(); } + @Test + public void testReadByteBufferInterrupted() throws HttpDataSourceException, InterruptedException { + mockResponseStartSuccess(); + dataSourceUnderTest.open(testDataSpec); + + final ConditionVariable startCondition = buildReadStartedCondition(); + final CountDownLatch timedOutLatch = new CountDownLatch(1); + ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8); + Thread thread = + new Thread() { + @Override + public void run() { + try { + dataSourceUnderTest.read(returnedBuffer); + fail(); + } catch (HttpDataSourceException e) { + // Expected. + assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue(); + timedOutLatch.countDown(); + } + } + }; + thread.start(); + startCondition.block(); + + assertNotCountedDown(timedOutLatch); + // Now we interrupt. + thread.interrupt(); + timedOutLatch.await(); + } + @Test public void testAllowDirectExecutor() throws HttpDataSourceException { testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); @@ -1064,4 +1379,17 @@ public final class CronetDataSourceTest { testBuffer.flip(); return testBuffer; } + + // Returns a copy of what is remaining in the src buffer from the current position to capacity. + private static byte[] copyByteBufferToArray(ByteBuffer src) { + if (src == null) { + return null; + } + byte[] copy = new byte[src.remaining()]; + int index = 0; + while (src.hasRemaining()) { + copy[index++] = src.get(); + } + return copy; + } } From 8a2871ed51d336a84c1dd2ea2306f33895aa2c3e Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 7 Jun 2019 01:26:45 +0100 Subject: [PATCH 1354/1556] Allow protected access to surface in MediaCodecVideoRenderer PiperOrigin-RevId: 251961318 --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 4 ++++ 1 file changed, 4 insertions(+) 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 45ab06db45..f60dbf3cb7 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 @@ -1603,6 +1603,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return deviceNeedsSetOutputSurfaceWorkaround; } + protected Surface getSurface() { + return surface; + } + protected static final class CodecMaxValues { public final int width; From 3bff79f56f5c2f80f225f626b86166034300017d Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 7 Jun 2019 16:35:56 +0100 Subject: [PATCH 1355/1556] Wrap MediaCodec exceptions in DecoderException and report as renderer error. We currently report MediaCodec exceptions as unexpected exceptions instead of as renderer error. All such exceptions are now wrapped in a new DecoderException to allow adding more details to the exception. PiperOrigin-RevId: 252054486 --- RELEASENOTES.md | 2 + .../exoplayer2/demo/PlayerActivity.java | 4 +- .../mediacodec/MediaCodecRenderer.java | 130 +++++++++++++----- .../video/MediaCodecVideoRenderer.java | 23 ++++ 4 files changed, 119 insertions(+), 40 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5128abba46..bc9f64a001 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,6 +17,8 @@ * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). * Add VR player demo. +* Wrap decoder exceptions in a new `DecoderException` class and report as + renderer error. ### 2.10.2 ### 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 82fb8bb9f5..929b579b4c 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 @@ -680,7 +680,7 @@ public class PlayerActivity extends AppCompatActivity // Special case for decoder initialization failures. DecoderInitializationException decoderInitializationException = (DecoderInitializationException) cause; - if (decoderInitializationException.decoderName == null) { + if (decoderInitializationException.codecInfo == null) { if (decoderInitializationException.getCause() instanceof DecoderQueryException) { errorString = getString(R.string.error_querying_decoders); } else if (decoderInitializationException.secureDecoderRequired) { @@ -695,7 +695,7 @@ public class PlayerActivity extends AppCompatActivity errorString = getString( R.string.error_instantiating_decoder, - decoderInitializationException.decoderName); + decoderInitializationException.codecInfo.name); } } } 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 d00b218c38..4b7bab2cfa 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 @@ -80,14 +80,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { public final boolean secureDecoderRequired; /** - * The name of the decoder that failed to initialize. Null if no suitable decoder was found. + * The {@link MediaCodecInfo} of the decoder that failed to initialize. Null if no suitable + * decoder was found. */ - public final String decoderName; + @Nullable public final MediaCodecInfo codecInfo; - /** - * An optional developer-readable diagnostic information string. May be null. - */ - public final String diagnosticInfo; + /** An optional developer-readable diagnostic information string. May be null. */ + @Nullable public final String diagnosticInfo; /** * If the decoder failed to initialize and another decoder being used as a fallback also failed @@ -103,19 +102,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer { cause, format.sampleMimeType, secureDecoderRequired, - /* decoderName= */ null, + /* mediaCodecInfo= */ null, buildCustomDiagnosticInfo(errorCode), /* fallbackDecoderInitializationException= */ null); } - public DecoderInitializationException(Format format, Throwable cause, - boolean secureDecoderRequired, String decoderName) { + public DecoderInitializationException( + Format format, + Throwable cause, + boolean secureDecoderRequired, + MediaCodecInfo mediaCodecInfo) { this( - "Decoder init failed: " + decoderName + ", " + format, + "Decoder init failed: " + mediaCodecInfo.name + ", " + format, cause, format.sampleMimeType, secureDecoderRequired, - decoderName, + mediaCodecInfo, Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null, /* fallbackDecoderInitializationException= */ null); } @@ -125,13 +127,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { Throwable cause, String mimeType, boolean secureDecoderRequired, - @Nullable String decoderName, + @Nullable MediaCodecInfo mediaCodecInfo, @Nullable String diagnosticInfo, @Nullable DecoderInitializationException fallbackDecoderInitializationException) { super(message, cause); this.mimeType = mimeType; this.secureDecoderRequired = secureDecoderRequired; - this.decoderName = decoderName; + this.codecInfo = mediaCodecInfo; this.diagnosticInfo = diagnosticInfo; this.fallbackDecoderInitializationException = fallbackDecoderInitializationException; } @@ -144,7 +146,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { getCause(), mimeType, secureDecoderRequired, - decoderName, + codecInfo, diagnosticInfo, fallbackException); } @@ -159,9 +161,34 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private static String buildCustomDiagnosticInfo(int errorCode) { String sign = errorCode < 0 ? "neg_" : ""; - return "com.google.android.exoplayer.MediaCodecTrackRenderer_" + sign + Math.abs(errorCode); + return "com.google.android.exoplayer2.mediacodec.MediaCodecRenderer_" + + sign + + Math.abs(errorCode); + } + } + + /** Thrown when a failure occurs in the decoder. */ + public static class DecoderException extends Exception { + + /** The {@link MediaCodecInfo} of the decoder that failed. Null if unknown. */ + @Nullable public final MediaCodecInfo codecInfo; + + /** An optional developer-readable diagnostic information string. May be null. */ + @Nullable public final String diagnosticInfo; + + public DecoderException(Throwable cause, @Nullable MediaCodecInfo codecInfo) { + super("Decoder failed: " + (codecInfo == null ? null : codecInfo.name), cause); + this.codecInfo = codecInfo; + diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null; } + @TargetApi(21) + private static String getDiagnosticInfoV21(Throwable cause) { + if (cause instanceof CodecException) { + return ((CodecException) cause).getDiagnosticInfo(); + } + return null; + } } /** Indicates no codec operating rate should be set. */ @@ -637,31 +664,40 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (outputStreamEnded) { - renderToEndOfStream(); - return; - } - if (inputFormat == null && !readToFlagsOnlyBuffer(/* requireFormat= */ true)) { + try { + if (outputStreamEnded) { + renderToEndOfStream(); + return; + } + if (inputFormat == null && !readToFlagsOnlyBuffer(/* requireFormat= */ true)) { // We still don't have a format and can't make progress without one. return; + } + // We have a format. + maybeInitCodec(); + if (codec != null) { + long drainStartTimeMs = SystemClock.elapsedRealtime(); + TraceUtil.beginSection("drainAndFeed"); + while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} + while (feedInputBuffer() && shouldContinueFeeding(drainStartTimeMs)) {} + TraceUtil.endSection(); + } else { + decoderCounters.skippedInputBufferCount += skipSource(positionUs); + // We need to read any format changes despite not having a codec so that drmSession can be + // updated, and so that we have the most recent format should the codec be initialized. We + // may + // also reach the end of the stream. Note that readSource will not read a sample into a + // flags-only buffer. + readToFlagsOnlyBuffer(/* requireFormat= */ false); + } + decoderCounters.ensureUpdated(); + } catch (IllegalStateException e) { + if (isMediaCodecException(e)) { + throw ExoPlaybackException.createForRenderer( + createDecoderException(e, getCodecInfo()), getIndex()); + } + throw e; } - // We have a format. - maybeInitCodec(); - if (codec != null) { - long drainStartTimeMs = SystemClock.elapsedRealtime(); - TraceUtil.beginSection("drainAndFeed"); - while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} - while (feedInputBuffer() && shouldContinueFeeding(drainStartTimeMs)) {} - TraceUtil.endSection(); - } else { - decoderCounters.skippedInputBufferCount += skipSource(positionUs); - // We need to read any format changes despite not having a codec so that drmSession can be - // updated, and so that we have the most recent format should the codec be initialized. We may - // also reach the end of the stream. Note that readSource will not read a sample into a - // flags-only buffer. - readToFlagsOnlyBuffer(/* requireFormat= */ false); - } - decoderCounters.ensureUpdated(); } /** @@ -725,6 +761,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } + protected DecoderException createDecoderException( + Throwable cause, @Nullable MediaCodecInfo codecInfo) { + return new DecoderException(cause, codecInfo); + } + /** Reads into {@link #flagsOnlyBuffer} and returns whether a format was read. */ private boolean readToFlagsOnlyBuffer(boolean requireFormat) throws ExoPlaybackException { flagsOnlyBuffer.clear(); @@ -785,7 +826,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { availableCodecInfos.removeFirst(); DecoderInitializationException exception = new DecoderInitializationException( - inputFormat, e, mediaCryptoRequiresSecureDecoder, codecInfo.name); + inputFormat, e, mediaCryptoRequiresSecureDecoder, codecInfo); if (preferredDecoderInitializationException == null) { preferredDecoderInitializationException = exception; } else { @@ -1701,6 +1742,19 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return cryptoInfo; } + private static boolean isMediaCodecException(IllegalStateException error) { + if (Util.SDK_INT >= 21) { + return isMediaCodecExceptionV21(error); + } + StackTraceElement[] stackTrace = error.getStackTrace(); + return stackTrace.length > 0 && stackTrace[0].getClassName().equals("android.media.MediaCodec"); + } + + @TargetApi(21) + private static boolean isMediaCodecExceptionV21(IllegalStateException error) { + return error instanceof MediaCodec.CodecException; + } + /** * Returns whether the device needs keys to have been loaded into the {@link DrmSession} before * codec configuration. 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 f60dbf3cb7..c864adfa68 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 @@ -92,6 +92,23 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { */ private static final float INITIAL_FORMAT_MAX_INPUT_SIZE_SCALE_FACTOR = 1.5f; + /** A {@link DecoderException} with additional surface information. */ + public static final class VideoDecoderException extends DecoderException { + + /** The {@link System#identityHashCode(Object)} of the surface when the exception occurred. */ + public final int surfaceIdentityHashCode; + + /** Whether the surface was valid when the exception occurred. */ + public final boolean isSurfaceValid; + + public VideoDecoderException( + Throwable cause, @Nullable MediaCodecInfo codecInfo, @Nullable Surface surface) { + super(cause, codecInfo); + surfaceIdentityHashCode = System.identityHashCode(surface); + isSurfaceValid = surface == null || surface.isValid(); + } + } + private static boolean evaluatedDeviceNeedsSetOutputSurfaceWorkaround; private static boolean deviceNeedsSetOutputSurfaceWorkaround; @@ -1260,6 +1277,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); } + @Override + protected DecoderException createDecoderException( + Throwable cause, @Nullable MediaCodecInfo codecInfo) { + return new VideoDecoderException(cause, codecInfo, surface); + } + /** * Returns a maximum video size to use when configuring a codec for {@code format} in a way that * will allow possible adaptation to other compatible formats that are expected to have the same From cc337a3e2d42118acfa32bd33eff0b7d207605ca Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 7 Jun 2019 23:09:13 +0100 Subject: [PATCH 1356/1556] Update nullness annotations. PiperOrigin-RevId: 252127811 --- .../exoplayer2/source/chunk/BaseMediaChunk.java | 3 ++- .../android/exoplayer2/source/chunk/ChunkHolder.java | 8 ++++---- .../exoplayer2/source/chunk/ChunkSampleStream.java | 12 ++++++------ .../android/exoplayer2/source/chunk/MediaChunk.java | 3 ++- 4 files changed, 14 insertions(+), 12 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 68322c60a1..74d8ddad3d 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.upstream.DataSource; @@ -58,7 +59,7 @@ public abstract class BaseMediaChunk extends MediaChunk { DataSpec dataSpec, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, + @Nullable Object trackSelectionData, long startTimeUs, long endTimeUs, long clippedStartTimeUs, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java index 6b7f5688ae..d6400c5165 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java @@ -15,15 +15,15 @@ */ package com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; + /** * Holds a chunk or an indication that the end of the stream has been reached. */ public final class ChunkHolder { - /** - * The chunk. - */ - public Chunk chunk; + /** The chunk. */ + @Nullable public Chunk chunk; /** * Indicates that the end of the stream has been reached. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index d9b28d9c92..499aea6a0c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -60,8 +60,8 @@ public class ChunkSampleStream implements SampleStream, S public final int primaryTrackType; - private final int[] embeddedTrackTypes; - private final Format[] embeddedTrackFormats; + @Nullable private final int[] embeddedTrackTypes; + @Nullable private final Format[] embeddedTrackFormats; private final boolean[] embeddedTracksSelected; private final T chunkSource; private final SequenceableLoader.Callback> callback; @@ -104,8 +104,8 @@ public class ChunkSampleStream implements SampleStream, S @Deprecated public ChunkSampleStream( int primaryTrackType, - int[] embeddedTrackTypes, - Format[] embeddedTrackFormats, + @Nullable int[] embeddedTrackTypes, + @Nullable Format[] embeddedTrackFormats, T chunkSource, Callback> callback, Allocator allocator, @@ -140,8 +140,8 @@ public class ChunkSampleStream implements SampleStream, S */ public ChunkSampleStream( int primaryTrackType, - int[] embeddedTrackTypes, - Format[] embeddedTrackFormats, + @Nullable int[] embeddedTrackTypes, + @Nullable Format[] embeddedTrackFormats, T chunkSource, Callback> callback, Allocator allocator, 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 9626f4b03f..39c097826f 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.upstream.DataSource; @@ -44,7 +45,7 @@ public abstract class MediaChunk extends Chunk { DataSpec dataSpec, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, + @Nullable Object trackSelectionData, long startTimeUs, long endTimeUs, long chunkIndex) { From 3fcae68432ae1cd07b8293cba0cb490f5aefdb4b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 13 Jun 2019 12:57:09 +0100 Subject: [PATCH 1357/1556] Add flags to DrmSessionManager PiperOrigin-RevId: 253006112 --- .../exoplayer2/drm/DrmSessionManager.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java index 168783cf1c..375faff797 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java @@ -16,13 +16,37 @@ package com.google.android.exoplayer2.drm; import android.os.Looper; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Manages a DRM session. */ public interface DrmSessionManager { + /** Flags that control the handling of DRM protected content. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS}) + @interface Flags {} + + /** + * When this flag is set, clear samples of an encrypted region may be rendered when no keys are + * available. + * + *

    Encrypted media may contain clear (un-encrypted) regions. For example a media file may start + * with a short clear region so as to allow playback to begin in parallel with key acquisition. + * When this flag is set, consumers of sample data are permitted to access the clear regions of + * encrypted media files when the associated {@link DrmSession} has not yet obtained the keys + * necessary for the encrypted regions of the media. + */ + int FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS = 1; + /** * Returns whether the manager is capable of acquiring a session for the given * {@link DrmInitData}. @@ -45,4 +69,10 @@ public interface DrmSessionManager { * @return The DRM session. */ DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData); + + /** Returns flags that control the handling of DRM protected content. */ + @Flags + default int getFlags() { + return 0; + } } From 2ce28a1620672faced2a9e08931c40f3053b397c Mon Sep 17 00:00:00 2001 From: arodriguez Date: Fri, 14 Jun 2019 08:24:31 +0200 Subject: [PATCH 1358/1556] Support for UDP data source --- .../exoplayer2/upstream/DefaultDataSource.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java index bfc9a37844..aeaa977b12 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -55,6 +55,7 @@ public final class DefaultDataSource implements DataSource { private static final String SCHEME_ASSET = "asset"; private static final String SCHEME_CONTENT = "content"; private static final String SCHEME_RTMP = "rtmp"; + private static final String SCHEME_UDP = "udp"; private static final String SCHEME_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME; private final Context context; @@ -66,6 +67,7 @@ public final class DefaultDataSource implements DataSource { @Nullable private DataSource assetDataSource; @Nullable private DataSource contentDataSource; @Nullable private DataSource rtmpDataSource; + @Nullable private DataSource udpDataSource; @Nullable private DataSource dataSchemeDataSource; @Nullable private DataSource rawResourceDataSource; @@ -139,6 +141,7 @@ public final class DefaultDataSource implements DataSource { maybeAddListenerToDataSource(assetDataSource, transferListener); maybeAddListenerToDataSource(contentDataSource, transferListener); maybeAddListenerToDataSource(rtmpDataSource, transferListener); + maybeAddListenerToDataSource(udpDataSource, transferListener); maybeAddListenerToDataSource(dataSchemeDataSource, transferListener); maybeAddListenerToDataSource(rawResourceDataSource, transferListener); } @@ -161,6 +164,8 @@ public final class DefaultDataSource implements DataSource { dataSource = getContentDataSource(); } else if (SCHEME_RTMP.equals(scheme)) { dataSource = getRtmpDataSource(); + } else if(SCHEME_UDP.equals(scheme)){ + dataSource = getUdpDataSource(); } else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) { dataSource = getDataSchemeDataSource(); } else if (SCHEME_RAW.equals(scheme)) { @@ -199,6 +204,14 @@ public final class DefaultDataSource implements DataSource { } } + private DataSource getUdpDataSource(){ + if (udpDataSource == null) { + udpDataSource = new UdpDataSource(); + addListenersToDataSource(udpDataSource); + } + return udpDataSource; + } + private DataSource getFileDataSource() { if (fileDataSource == null) { fileDataSource = new FileDataSource(); From 04524a688ded24b108abc574360f1902e077860c Mon Sep 17 00:00:00 2001 From: Tim Balsfulland Date: Sat, 15 Jun 2019 15:59:57 +0200 Subject: [PATCH 1359/1556] Add convenience constructors for notification channel descriptions --- .../exoplayer2/offline/DownloadService.java | 44 ++++++++++++- .../exoplayer2/util/NotificationUtil.java | 37 +++++++++++ .../ui/PlayerNotificationManager.java | 61 +++++++++++++++++++ 3 files changed, 141 insertions(+), 1 deletion(-) 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 3900dc8e93..2110ac2c48 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 @@ -174,6 +174,7 @@ public abstract class DownloadService extends Service { @Nullable private final ForegroundNotificationUpdater foregroundNotificationUpdater; @Nullable private final String channelId; @StringRes private final int channelNameResourceId; + @StringRes private final int channelDescriptionResourceId; private DownloadManager downloadManager; private int lastStartId; @@ -239,16 +240,53 @@ public abstract class DownloadService extends Service { long foregroundNotificationUpdateInterval, @Nullable String channelId, @StringRes int channelNameResourceId) { + this( + foregroundNotificationId, + foregroundNotificationUpdateInterval, + channelId, + channelNameResourceId, + /* channelDescriptionResourceId= */ 0 + ); + } + + /** + * Creates a DownloadService. + * + * @param foregroundNotificationId The notification id for the foreground notification, or {@link + * #FOREGROUND_NOTIFICATION_ID_NONE} if the service should only ever run in the background. + * @param foregroundNotificationUpdateInterval The maximum interval between updates to the + * foreground notification, in milliseconds. Ignored if {@code foregroundNotificationId} is + * {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + * @param channelId An id for a low priority notification channel to create, or {@code null} if + * the app will take care of creating a notification channel if needed. If specified, must be + * unique per package. The value may be truncated if it's too long. Ignored if {@code + * foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + * @param channelNameResourceId A string resource identifier for the user visible name of the + * channel, if {@code channelId} is specified. The recommended maximum length is 40 + * characters. The value may be truncated if it is too long. Ignored if {@code + * foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + * @param channelDescriptionResourceId A string resource identifier for the user visible + * description. Ignored if {@code foregroundNotificationId} is + * {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + */ + protected DownloadService( + int foregroundNotificationId, + long foregroundNotificationUpdateInterval, + @Nullable String channelId, + @StringRes int channelNameResourceId, + @StringRes int channelDescriptionResourceId) { if (foregroundNotificationId == FOREGROUND_NOTIFICATION_ID_NONE) { this.foregroundNotificationUpdater = null; this.channelId = null; this.channelNameResourceId = 0; + this.channelDescriptionResourceId = 0; } else { this.foregroundNotificationUpdater = new ForegroundNotificationUpdater( foregroundNotificationId, foregroundNotificationUpdateInterval); this.channelId = channelId; this.channelNameResourceId = channelNameResourceId; + this.channelDescriptionResourceId = channelDescriptionResourceId; } } @@ -543,7 +581,11 @@ public abstract class DownloadService extends Service { public void onCreate() { if (channelId != null) { NotificationUtil.createNotificationChannel( - this, channelId, channelNameResourceId, NotificationUtil.IMPORTANCE_LOW); + this, + channelId, + channelNameResourceId, + channelDescriptionResourceId, + NotificationUtil.IMPORTANCE_LOW); } Class clazz = getClass(); DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(clazz); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java index 4cd03f566d..910a8efbe9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java @@ -80,11 +80,48 @@ public final class NotificationUtil { */ public static void createNotificationChannel( Context context, String id, @StringRes int nameResourceId, @Importance int importance) { + createNotificationChannel( + context, + id, + nameResourceId, + importance, + /* descriptionResourceId= */ 0); + } + + /** + * Creates a notification channel that notifications can be posted to. See {@link + * NotificationChannel} and {@link + * NotificationManager#createNotificationChannel(NotificationChannel)} for details. + * + * @param context A {@link Context}. + * @param id The id of the channel. Must be unique per package. The value may be truncated if it's + * too long. + * @param nameResourceId A string resource identifier for the user visible name of the channel. + * You can rename this channel when the system locale changes by listening for the {@link + * Intent#ACTION_LOCALE_CHANGED} broadcast. The recommended maximum length is 40 characters. + * The value may be truncated if it is too long. + * @param importance The importance of the channel. This controls how interruptive notifications + * posted to this channel are. One of {@link #IMPORTANCE_UNSPECIFIED}, {@link + * #IMPORTANCE_NONE}, {@link #IMPORTANCE_MIN}, {@link #IMPORTANCE_LOW}, {@link + * #IMPORTANCE_DEFAULT} and {@link #IMPORTANCE_HIGH}. + * @param descriptionResourceId A String resource identifier for the user visible description of + * the channel. You can change the description of this channel when the system locale changes + * by listening for the {@link Intent#ACTION_LOCALE_CHANGED} broadcast. Ignored if set to 0. + */ + public static void createNotificationChannel( + Context context, + String id, + @StringRes int nameResourceId, + @Importance int importance, + @StringRes int descriptionResourceId) { if (Util.SDK_INT >= 26) { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); NotificationChannel channel = new NotificationChannel(id, context.getString(nameResourceId), importance); + if(descriptionResourceId != 0) { + channel.setDescription(context.getString(descriptionResourceId)); + } notificationManager.createNotificationChannel(channel); } } 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 aa9e4b1492..0c34cc7bba 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 @@ -414,6 +414,38 @@ public class PlayerNotificationManager { context, channelId, notificationId, mediaDescriptionAdapter); } + /** + * Creates a notification manager and a low-priority notification channel with the specified + * {@code channelId} and {@code channelName}. + * + *

    If the player notification manager is intended to be used within a foreground service, + * {@link #createWithNotificationChannel(Context, String, int, int, MediaDescriptionAdapter, + * NotificationListener)} should be used to which a {@link NotificationListener} can be passed. + * This way you'll receive the notification to put the service into the foreground by calling + * {@link android.app.Service#startForeground(int, Notification)}. + * + * @param context The {@link Context}. + * @param channelId The id of the notification channel. + * @param channelName A string resource identifier for the user visible name of the channel. The + * recommended maximum length is 40 characters; the value may be truncated if it is too long. + * @param channelDescription A String resource identifier for the user visible description of the + * channel. + * @param notificationId The id of the notification. + * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. + */ + public static PlayerNotificationManager createWithNotificationChannel( + Context context, + String channelId, + @StringRes int channelName, + @StringRes int channelDescription, + int notificationId, + MediaDescriptionAdapter mediaDescriptionAdapter) { + NotificationUtil.createNotificationChannel( + context, channelId, channelName, channelDescription, NotificationUtil.IMPORTANCE_LOW); + return new PlayerNotificationManager( + context, channelId, notificationId, mediaDescriptionAdapter); + } + /** * Creates a notification manager and a low-priority notification channel with the specified * {@code channelId} and {@code channelName}. The {@link NotificationListener} passed as the last @@ -440,6 +472,35 @@ public class PlayerNotificationManager { context, channelId, notificationId, mediaDescriptionAdapter, notificationListener); } + /** + * Creates a notification manager and a low-priority notification channel with the specified + * {@code channelId} and {@code channelName}. The {@link NotificationListener} passed as the last + * parameter will be notified when the notification is created and cancelled. + * + * @param context The {@link Context}. + * @param channelId The id of the notification channel. + * @param channelName A string resource identifier for the user visible name of the channel. The + * recommended maximum length is 40 characters; the value may be truncated if it is too long. + * @param channelDescription A String resource identifier for the user visible description of the + * channel. + * @param notificationId The id of the notification. + * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. + * @param notificationListener The {@link NotificationListener}. + */ + public static PlayerNotificationManager createWithNotificationChannel( + Context context, + String channelId, + @StringRes int channelName, + @StringRes int channelDescription, + int notificationId, + MediaDescriptionAdapter mediaDescriptionAdapter, + @Nullable NotificationListener notificationListener) { + NotificationUtil.createNotificationChannel( + context, channelId, channelName, channelDescription, NotificationUtil.IMPORTANCE_LOW); + return new PlayerNotificationManager( + context, channelId, notificationId, mediaDescriptionAdapter, notificationListener); + } + /** * Creates a notification manager using the specified notification {@code channelId}. The caller * is responsible for creating the notification channel. From b29731d501c9ee97bdc3248f2a20e3c0e9374ea8 Mon Sep 17 00:00:00 2001 From: Yannick RUI Date: Tue, 18 Jun 2019 11:27:37 +0200 Subject: [PATCH 1360/1556] Parse text track subtype into Format.roleflags. --- .../manifest/SsManifestParser.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java index 66731660f5..7b7c539aee 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java @@ -586,6 +586,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { } else { subType = parser.getAttributeValue(null, KEY_SUB_TYPE); } + putNormalizedAttribute(KEY_SUB_TYPE, subType); name = parser.getAttributeValue(null, KEY_NAME); url = parseRequiredString(parser, KEY_URL); maxWidth = parseInt(parser, KEY_MAX_WIDTH, Format.NO_VALUE); @@ -645,6 +646,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { private static final String KEY_CHANNELS = "Channels"; private static final String KEY_FOUR_CC = "FourCC"; private static final String KEY_TYPE = "Type"; + private static final String KEY_SUB_TYPE = "Subtype"; private static final String KEY_LANGUAGE = "Language"; private static final String KEY_NAME = "Name"; private static final String KEY_MAX_WIDTH = "MaxWidth"; @@ -710,6 +712,18 @@ public class SsManifestParser implements ParsingLoadable.Parser { language); } else if (type == C.TRACK_TYPE_TEXT) { String language = (String) getNormalizedAttribute(KEY_LANGUAGE); + String subType = (String) getNormalizedAttribute(KEY_SUB_TYPE); + int roleFlags = 0; + switch (subType) { + case "CAPT": + roleFlags |= C.ROLE_FLAG_CAPTION; + break; + case "DESC": + roleFlags |= C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND; + break; + case "SUBT": + break; + } format = Format.createTextContainerFormat( id, @@ -719,7 +733,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { /* codecs= */ null, bitrate, /* selectionFlags= */ 0, - /* roleFlags= */ 0, + /* roleFlags= */ roleFlags, language); } else { format = From 1266d5967be77af9f0be5636d10eea940e983b43 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 17 Jun 2019 09:56:50 +0100 Subject: [PATCH 1361/1556] Fix all FIXME comments. These are mostly nullability issues. PiperOrigin-RevId: 253537068 --- .../android/exoplayer2/drm/DefaultDrmSessionManager.java | 2 +- .../java/com/google/android/exoplayer2/drm/ExoMediaDrm.java | 3 ++- .../google/android/exoplayer2/drm/FrameworkMediaDrm.java | 5 +---- .../android/exoplayer2/scheduler/PlatformScheduler.java | 6 +++--- 4 files changed, 7 insertions(+), 9 deletions(-) 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 84e984445a..4e18df04e3 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 @@ -533,7 +533,7 @@ public class DefaultDrmSessionManager @Override public void onEvent( ExoMediaDrm md, - byte[] sessionId, + @Nullable byte[] sessionId, int event, int extra, @Nullable byte[] data) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java index 49915f3af5..6bd8d9688f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java @@ -80,7 +80,7 @@ public interface ExoMediaDrm { */ void onEvent( ExoMediaDrm mediaDrm, - byte[] sessionId, + @Nullable byte[] sessionId, int event, int extra, @Nullable byte[] data); @@ -215,6 +215,7 @@ public interface ExoMediaDrm { throws NotProvisionedException; /** @see MediaDrm#provideKeyResponse(byte[], byte[]) */ + @Nullable byte[] provideKeyResponse(byte[] scope, byte[] response) throws NotProvisionedException, DeniedByServerException; 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 848d9e146a..609abd4e1e 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 @@ -84,8 +84,6 @@ public final class FrameworkMediaDrm implements ExoMediaDrm listener) { @@ -160,8 +158,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm Date: Mon, 17 Jun 2019 14:26:55 +0100 Subject: [PATCH 1362/1556] Remove Objects.equals use from CronetDataSource Objects was added in API 19. PiperOrigin-RevId: 253567490 --- .../android/exoplayer2/ext/cronet/CronetDataSource.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index 7e30d924a0..2cd40c8d70 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Predicate; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; @@ -40,7 +41,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Objects; import java.util.concurrent.Executor; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -759,7 +759,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } catch (InterruptedException e) { // The operation is ongoing so replace buffer to avoid it being written to by this // operation during a subsequent request. - if (Objects.equals(buffer, readBuffer)) { + if (Util.areEqual(buffer, readBuffer)) { readBuffer = null; } Thread.currentThread().interrupt(); @@ -770,7 +770,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } catch (SocketTimeoutException e) { // The operation is ongoing so replace buffer to avoid it being written to by this // operation during a subsequent request. - if (Objects.equals(buffer, readBuffer)) { + if (Util.areEqual(buffer, readBuffer)) { readBuffer = null; } throw new HttpDataSourceException( From f90cbcdffd066761789fa40c87147992ae819b92 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 17 Jun 2019 16:30:10 +0100 Subject: [PATCH 1363/1556] Add MRC continuous play API to IMA android sdk. Details in go/ima-mrc-continuous-play Corresponding js webcore changes is in . NoExternal PiperOrigin-RevId: 253585186 --- .../google/android/exoplayer2/ext/ima/FakeAdsRequest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java index 7c2c8a6e0b..3c34d9b577 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java @@ -105,6 +105,11 @@ public final class FakeAdsRequest implements AdsRequest { throw new UnsupportedOperationException(); } + @Override + public void setContinuousPlayback(boolean b) { + throw new UnsupportedOperationException(); + } + @Override public void setContentDuration(float v) { throw new UnsupportedOperationException(); From c05cb3f6f46a9c03e7de913c151e8155e1f20aeb Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 17 Jun 2019 17:13:32 +0100 Subject: [PATCH 1364/1556] Add bug report section to question and content_not_playing issue templates. PiperOrigin-RevId: 253593267 --- .github/ISSUE_TEMPLATE/bug.md | 9 +++++---- .github/ISSUE_TEMPLATE/content_not_playing.md | 14 +++++++++++--- .github/ISSUE_TEMPLATE/question.md | 17 +++++++++++++++++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index a4996278bd..c0980df440 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -36,16 +36,17 @@ or a small sample app that you’re able to share as source code on GitHub. Provide a JSON snippet for the demo app’s media.exolist.json file, or a link to media that reproduces the issue. If you don't wish to post it publicly, please submit the issue, then email the link to dev.exoplayer@gmail.com using a subject -in the format "Issue #1234". Provide all the metadata we'd need to play the -content like drm license urls or similar. If the content is accessible only in -certain countries or regions, please say so. +in the format "Issue #1234", where "#1234" should be replaced with your issue +number. Provide all the metadata we'd need to play the content like drm license +urls or similar. If the content is accessible only in certain countries or +regions, please say so. ### [REQUIRED] A full bug report captured from the device Capture a full bug report using "adb bugreport". Output from "adb logcat" or a log snippet is NOT sufficient. Please attach the captured bug report as a file. If you don't wish to post it publicly, please submit the issue, then email the bug report to dev.exoplayer@gmail.com using a subject in the format -"Issue #1234". +"Issue #1234", where "#1234" should be replaced with your issue number. ### [REQUIRED] Version of ExoPlayer being used Specify the absolute version number. Avoid using terms such as "latest". diff --git a/.github/ISSUE_TEMPLATE/content_not_playing.md b/.github/ISSUE_TEMPLATE/content_not_playing.md index ff29f3a7d1..c8d4668a6a 100644 --- a/.github/ISSUE_TEMPLATE/content_not_playing.md +++ b/.github/ISSUE_TEMPLATE/content_not_playing.md @@ -33,9 +33,10 @@ and you expect to play, like 5.1 audio track, text tracks or drm systems. Provide a JSON snippet for the demo app’s media.exolist.json file, or a link to media that reproduces the issue. If you don't wish to post it publicly, please submit the issue, then email the link to dev.exoplayer@gmail.com using a subject -in the format "Issue #1234". Provide all the metadata we'd need to play the -content like drm license urls or similar. If the content is accessible only in -certain countries or regions, please say so. +in the format "Issue #1234", where "#1234" should be replaced with your issue +number. Provide all the metadata we'd need to play the content like drm license +urls or similar. If the content is accessible only in certain countries or +regions, please say so. ### [REQUIRED] Version of ExoPlayer being used Specify the absolute version number. Avoid using terms such as "latest". @@ -44,6 +45,13 @@ Specify the absolute version number. Avoid using terms such as "latest". Specify the devices and versions of Android on which you expect the content to play. If possible, please test on multiple devices and Android versions. +### [REQUIRED] A full bug report captured from the device +Capture a full bug report using "adb bugreport". Output from "adb logcat" or a +log snippet is NOT sufficient. Please attach the captured bug report as a file. +If you don't wish to post it publicly, please submit the issue, then email the +bug report to dev.exoplayer@gmail.com using a subject in the format +"Issue #1234", where "#1234" should be replaced with your issue number. + + + + + + +

      +
      + +
      +
      +
      +
      +
      + + +
      +
      +
      + for debugging
      purpose only +
      +
      +
      +
        +
      • prepare
      • +
      • prev
      • +
      • rewind
      • +
      • play
      • +
      • pause
      • +
      • ffwd
      • +
      • next
      • +
      • stop
      • +
      +
      +
      +
      + + + diff --git a/cast_receiver_app/app-desktop/src/main.js b/cast_receiver_app/app-desktop/src/main.js new file mode 100644 index 0000000000..5645d70787 --- /dev/null +++ b/cast_receiver_app/app-desktop/src/main.js @@ -0,0 +1,170 @@ +/* + * 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. + */ + +goog.module('exoplayer.cast.debug'); + +const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); +const PlaybackInfoView = goog.require('exoplayer.cast.PlaybackInfoView'); +const Player = goog.require('exoplayer.cast.Player'); +const PlayerControls = goog.require('exoplayer.cast.PlayerControls'); +const ShakaPlayer = goog.require('shaka.Player'); +const SimpleTextDisplayer = goog.require('shaka.text.SimpleTextDisplayer'); +const installAll = goog.require('shaka.polyfill.installAll'); +const util = goog.require('exoplayer.cast.util'); + +/** @type {!Array} */ +let queue = []; +/** @type {number} */ +let uuidCounter = 1; + +// install all polyfills for the Shaka player +installAll(); + +/** + * Listens for player state changes and logs the state to the console. + * + * @param {!PlayerState} playerState The player state. + */ +const playerListener = function(playerState) { + util.log(['playerState: ', playerState.playbackPosition, playerState]); + queue = playerState.mediaQueue; + highlightCurrentItem( + playerState.playbackPosition && playerState.playbackPosition.uuid ? + playerState.playbackPosition.uuid : + ''); + if (playerState.playWhenReady && playerState.playbackState === 'READY') { + document.body.classList.add('playing'); + } else { + document.body.classList.remove('playing'); + } + if (playerState.playbackState === 'IDLE' && queue.length === 0) { + // Stop has been called or player not yet prepared. + resetSampleList(); + } +}; + +/** + * Highlights the currently playing item in the samples list. + * + * @param {string} uuid + */ +const highlightCurrentItem = function(uuid) { + const actions = /** @type {!NodeList} */ ( + document.querySelectorAll('#media-actions .action')); + for (let action of actions) { + if (action.dataset['uuid'] === uuid) { + action.classList.add('prepared'); + } else { + action.classList.remove('prepared'); + } + } +}; + +/** + * Makes sure all items reflect being removed from the timeline. + */ +const resetSampleList = function() { + const actions = /** @type {!NodeList} */ ( + document.querySelectorAll('#media-actions .action')); + for (let action of actions) { + action.classList.remove('prepared'); + delete action.dataset['uuid']; + } +}; + +/** + * If the arguments provide a valid media item it is added to the player. + * + * @param {!MediaItem} item The media item. + * @return {string} The uuid which has been created for the item before adding. + */ +const addQueueItem = function(item) { + if (!(item.media && item.media.uri && item.mimeType)) { + throw Error('insufficient arguments to add a queue item'); + } + item.uuid = 'uuid-' + uuidCounter++; + player.addQueueItems(queue.length, [item], /* playbackOrder= */ undefined); + return item.uuid; +}; + +/** + * An event listener which listens for actions. + * + * @param {!Event} ev The DOM event. + */ +const handleAction = (ev) => { + let target = ev.target; + while (target !== document.body && !target.dataset['action']) { + target = target.parentNode; + } + if (!target || !target.dataset['action']) { + return; + } + switch (target.dataset['action']) { + case 'player.addItems': + if (target.dataset['uuid']) { + player.removeQueueItems([target.dataset['uuid']]); + delete target.dataset['uuid']; + } else { + const uuid = addQueueItem(/** @type {!MediaItem} */ + (JSON.parse(target.dataset['item']))); + target.dataset['uuid'] = uuid; + } + break; + } +}; + +/** + * Appends samples to the list of media item actions. + * + * @param {!Array} mediaItems The samples to add. + */ +const appendSamples = function(mediaItems) { + const samplesList = document.getElementById('media-actions'); + mediaItems.forEach((item) => { + const div = /** @type {!HTMLElement} */ (document.createElement('div')); + div.classList.add('action', 'button'); + div.dataset['action'] = 'player.addItems'; + div.dataset['item'] = JSON.stringify(item); + div.appendChild(document.createTextNode(item.title)); + const marker = document.createElement('span'); + marker.classList.add('queue-marker'); + div.appendChild(marker); + samplesList.appendChild(div); + }); +}; + +/** @type {!HTMLMediaElement} */ +const mediaElement = + /** @type {!HTMLMediaElement} */ (document.getElementById('video')); +// Workaround for https://github.com/google/shaka-player/issues/1819 +// TODO(bachinger) Remove line when better fix available. +new SimpleTextDisplayer(mediaElement); +/** @type {!ShakaPlayer} */ +const shakaPlayer = new ShakaPlayer(mediaElement); +/** @type {!Player} */ +const player = new Player(shakaPlayer, new ConfigurationFactory()); +new PlayerControls(player, 'exo_controls'); +new PlaybackInfoView(player, 'exo_playback_info'); + +// register listeners +document.body.addEventListener('click', handleAction); +player.addPlayerListener(playerListener); + +// expose the player for debugging purposes. +window['player'] = player; + +exports.appendSamples = appendSamples; diff --git a/cast_receiver_app/app-desktop/src/player_controls.js b/cast_receiver_app/app-desktop/src/player_controls.js new file mode 100644 index 0000000000..e29f74148c --- /dev/null +++ b/cast_receiver_app/app-desktop/src/player_controls.js @@ -0,0 +1,164 @@ +/* + * 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. + */ +goog.module('exoplayer.cast.PlayerControls'); + +const Player = goog.require('exoplayer.cast.Player'); + +/** + * A simple UI to control the player. + * + */ +class PlayerControls { + /** + * @param {!Player} player The player. + * @param {string} containerId The id of the container element. + */ + constructor(player, containerId) { + /** @const @private {!Player} */ + this.player_ = player; + /** @const @private {?Element} */ + this.root_ = document.getElementById(containerId); + /** @const @private {?Element} */ + this.playButton_ = this.root_.querySelector('#button_play'); + /** @const @private {?Element} */ + this.pauseButton_ = this.root_.querySelector('#button_pause'); + /** @const @private {?Element} */ + this.previousButton_ = this.root_.querySelector('#button_previous'); + /** @const @private {?Element} */ + this.nextButton_ = this.root_.querySelector('#button_next'); + + const previous = () => { + const index = player.getPreviousWindowIndex(); + if (index !== -1) { + player.seekToWindow(index, 0); + } + }; + const next = () => { + const index = player.getNextWindowIndex(); + if (index !== -1) { + player.seekToWindow(index, 0); + } + }; + const rewind = () => { + player.seekToWindow( + player.getCurrentWindowIndex(), + player.getCurrentPositionMs() - 15000); + }; + const fastForward = () => { + player.seekToWindow( + player.getCurrentWindowIndex(), + player.getCurrentPositionMs() + 30000); + }; + const actions = { + 'pwr_1': (ev) => player.setPlayWhenReady(true), + 'pwr_0': (ev) => player.setPlayWhenReady(false), + 'rewind': rewind, + 'fastforward': fastForward, + 'previous': previous, + 'next': next, + 'prepare': (ev) => player.prepare(), + 'stop': (ev) => player.stop(true), + 'remove_queue_item': (ev) => { + player.removeQueueItems([ev.target.dataset.id]); + }, + }; + /** + * @param {!Event} ev The key event. + * @return {boolean} true if the key event has been handled. + */ + const keyListener = (ev) => { + const key = /** @type {!KeyboardEvent} */ (ev).key; + switch (key) { + case 'ArrowUp': + case 'k': + previous(); + ev.preventDefault(); + return true; + case 'ArrowDown': + case 'j': + next(); + ev.preventDefault(); + return true; + case 'ArrowLeft': + case 'h': + rewind(); + ev.preventDefault(); + return true; + case 'ArrowRight': + case 'l': + fastForward(); + ev.preventDefault(); + return true; + case ' ': + case 'p': + player.setPlayWhenReady(!player.getPlayWhenReady()); + ev.preventDefault(); + return true; + } + return false; + }; + document.addEventListener('keydown', keyListener); + this.root_.addEventListener('click', function(ev) { + const method = ev.target['dataset']['method']; + if (actions[method]) { + actions[method](ev); + } + return true; + }); + player.addPlayerListener((playerState) => this.updateUi(playerState)); + player.invalidate(); + this.setVisible_(true); + } + + /** + * Syncs the ui with the player state. + * + * @param {!PlayerState} playerState The state of the player to be reflected + * by the UI. + */ + updateUi(playerState) { + if (playerState.playWhenReady) { + this.playButton_.style.display = 'none'; + this.pauseButton_.style.display = 'inline-block'; + } else { + this.playButton_.style.display = 'inline-block'; + this.pauseButton_.style.display = 'none'; + } + if (this.player_.getNextWindowIndex() === -1) { + this.nextButton_.style.visibility = 'hidden'; + } else { + this.nextButton_.style.visibility = 'visible'; + } + if (this.player_.getPreviousWindowIndex() === -1) { + this.previousButton_.style.visibility = 'hidden'; + } else { + this.previousButton_.style.visibility = 'visible'; + } + } + + /** + * @private + * @param {boolean} visible If `true` thie controls are shown. If `false` the + * controls are hidden. + */ + setVisible_(visible) { + if (this.root_) { + this.root_.style.display = visible ? 'block' : 'none'; + } + } +} + +exports = PlayerControls; diff --git a/cast_receiver_app/app-desktop/src/samples.js b/cast_receiver_app/app-desktop/src/samples.js new file mode 100644 index 0000000000..2d190bdef4 --- /dev/null +++ b/cast_receiver_app/app-desktop/src/samples.js @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2019 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. + */ +goog.module('exoplayer.cast.samples'); + +const {appendSamples} = goog.require('exoplayer.cast.debug'); + +appendSamples([ + { + title: 'DASH: multi-period', + mimeType: 'application/dash+xml', + media: { + uri: 'https://storage.googleapis.com/exoplayer-test-media-internal-6383' + + '4241aced7884c2544af1a3452e01/dash/multi-period/two-periods-minimal' + + '-duration.mpd', + }, + }, + { + title: 'HLS: Angel one', + mimeType: 'application/vnd.apple.mpegurl', + media: { + uri: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hl' + + 's.m3u8', + }, + }, + { + title: 'MP4: Elephants dream', + mimeType: 'video/*', + media: { + uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/' + + 'ElephantsDream.mp4', + }, + }, + { + title: 'MKV: Android screens', + mimeType: 'video/*', + media: { + uri: 'https://storage.googleapis.com/exoplayer-test-media-1/mkv/android' + + '-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv', + }, + }, + { + title: 'WV: HDCP not specified', + mimeType: 'application/dash+xml', + media: { + uri: 'https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd', + }, + drmSchemes: [ + { + licenseServer: { + uri: 'https://proxy.uat.widevine.com/proxy?video_id=d286538032258a1' + + 'c&provider=widevine_test', + }, + uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', + }, + ], + }, +]); diff --git a/cast_receiver_app/app-desktop/src/samples_internal.js b/cast_receiver_app/app-desktop/src/samples_internal.js new file mode 100644 index 0000000000..71b05eb2c1 --- /dev/null +++ b/cast_receiver_app/app-desktop/src/samples_internal.js @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2019 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. + */ +goog.module('exoplayer.cast.samplesinternal'); + +const {appendSamples} = goog.require('exoplayer.cast.debug'); + +appendSamples([ + { + title: 'DAS: VOD', + mimeType: 'application/dash+xml', + media: { + uri: 'https://demo-dash-pvr.zahs.tv/hd/manifest.mpd', + }, + }, + { + title: 'MP3', + mimeType: 'audio/*', + media: { + uri: 'http://www.noiseaddicts.com/samples_1w72b820/4190.mp3', + }, + }, + { + title: 'DASH: live', + mimeType: 'application/dash+xml', + media: { + uri: 'https://demo-dash-live.zahs.tv/sd/manifest.mpd', + }, + }, + { + title: 'HLS: live', + mimeType: 'application/vnd.apple.mpegurl', + media: { + uri: 'https://demo-hls5-live.zahs.tv/sd/master.m3u8', + }, + }, + { + title: 'Live DASH (HD/Widevine)', + mimeType: 'application/dash+xml', + media: { + uri: 'https://demo-dashenc-live.zahs.tv/hd/widevine.mpd', + }, + drmSchemes: [ + { + licenseServer: { + uri: 'https://demo-dashenc-live.zahs.tv/hd/widevine-license', + }, + uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', + }, + ], + }, + { + title: 'VOD DASH (HD/Widevine)', + mimeType: 'application/dash+xml', + media: { + uri: 'https://demo-dashenc-pvr.zahs.tv/hd/widevine.mpd', + }, + drmSchemes: [ + { + licenseServer: { + uri: 'https://demo-dashenc-live.zahs.tv/hd/widevine-license', + }, + uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', + }, + ], + }, +]); diff --git a/cast_receiver_app/app/html/index.css b/cast_receiver_app/app/html/index.css new file mode 100644 index 0000000000..dfc9b4e0e5 --- /dev/null +++ b/cast_receiver_app/app/html/index.css @@ -0,0 +1,39 @@ +/* + * 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. + */ +section, video, div, span, body, html { + border: 0; + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body { + background-color: #000; + height: 100%; + overflow: hidden; +} + +#exo_player_view { + background-color: #000; + height: 100%; + position: relative; +} + +#exo_video { + height: 100%; + width: 100%; +} + diff --git a/cast_receiver_app/app/html/index.html b/cast_receiver_app/app/html/index.html new file mode 100644 index 0000000000..64de3e8a8e --- /dev/null +++ b/cast_receiver_app/app/html/index.html @@ -0,0 +1,40 @@ + + + + + + + + + +
      + +
      +
      +
      +
      +
      + + +
      +
      +
      + + + diff --git a/cast_receiver_app/app/html/playback_info_view.css b/cast_receiver_app/app/html/playback_info_view.css new file mode 100644 index 0000000000..f70695d873 --- /dev/null +++ b/cast_receiver_app/app/html/playback_info_view.css @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2019 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. + */ + +.exo_text_label { + color: #fff; + font-family: Roboto, Arial, sans-serif; + font-size: 1em; + margin-top: 4px; +} + +#exo_playback_info { + bottom: 5%; + display: none; + left: 4%; + position: absolute; + right: 4%; + width: 92%; +} + +#exo_time_bar { + width: 100%; +} + +#exo_duration { + background-color: rgba(255, 255, 255, 0.4); + height: 0.5em; + overflow: hidden; + position: relative; + width: 100%; +} + +#exo_elapsed_time { + background-color: rgb(73, 128, 218); + height: 100%; + opacity: 1; + width: 0; +} + +#exo_duration_label { + float: right; +} + +#exo_elapsed_time_label { + float: left; +} + diff --git a/cast_receiver_app/app/src/main.js b/cast_receiver_app/app/src/main.js new file mode 100644 index 0000000000..37c6fd41eb --- /dev/null +++ b/cast_receiver_app/app/src/main.js @@ -0,0 +1,55 @@ +/* + * 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. + */ + +goog.module('exoplayer.cast.app'); + +const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); +const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher'); +const PlaybackInfoView = goog.require('exoplayer.cast.PlaybackInfoView'); +const Player = goog.require('exoplayer.cast.Player'); +const Receiver = goog.require('exoplayer.cast.Receiver'); +const ShakaPlayer = goog.require('shaka.Player'); +const SimpleTextDisplayer = goog.require('shaka.text.SimpleTextDisplayer'); +const installAll = goog.require('shaka.polyfill.installAll'); + +/** + * The ExoPlayer namespace for messages sent and received via cast message bus. + */ +const MESSAGE_NAMESPACE_EXOPLAYER = 'urn:x-cast:com.google.exoplayer.cast'; + +// installs all polyfills for the Shaka player +installAll(); +/** @type {?HTMLMediaElement} */ +const videoElement = + /** @type {?HTMLMediaElement} */ (document.getElementById('exo_video')); +if (videoElement !== null) { + // Workaround for https://github.com/google/shaka-player/issues/1819 + // TODO(bachinger) Remove line when better fix available. + new SimpleTextDisplayer(videoElement); + /** @type {!cast.framework.CastReceiverContext} */ + const castReceiverContext = cast.framework.CastReceiverContext.getInstance(); + const shakaPlayer = new ShakaPlayer(/** @type {!HTMLMediaElement} */ + (videoElement)); + const player = new Player(shakaPlayer, new ConfigurationFactory()); + new PlaybackInfoView(player, 'exo_playback_info'); + if (castReceiverContext !== null) { + const messageDispatcher = + new MessageDispatcher(MESSAGE_NAMESPACE_EXOPLAYER, castReceiverContext); + new Receiver(player, castReceiverContext, messageDispatcher); + } + // expose player for debugging purposes. + window['player'] = player; +} diff --git a/cast_receiver_app/app/src/message_dispatcher.js b/cast_receiver_app/app/src/message_dispatcher.js new file mode 100644 index 0000000000..151ac87fbe --- /dev/null +++ b/cast_receiver_app/app/src/message_dispatcher.js @@ -0,0 +1,234 @@ +/* + * 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. + */ +goog.module('exoplayer.cast.MessageDispatcher'); + +const validation = goog.require('exoplayer.cast.validation'); + +/** + * A callback function which is called by an action handler to indicate when + * processing has completed. + * + * @typedef {function(?PlayerState): undefined} + */ +const Callback = undefined; + +/** + * Handles an action sent by a sender app. + * + * @typedef {function(!Object, number, string, !Callback): undefined} + */ +const ActionHandler = undefined; + +/** + * Dispatches messages of a cast message bus to registered action handlers. + * + *

      The dispatcher listens to events of a CastMessageBus for the namespace + * passed to the constructor. The data property of the event is + * parsed as a json document and delegated to a handler registered for the given + * method. + */ +class MessageDispatcher { + /** + * @param {string} namespace The message namespace. + * @param {!cast.framework.CastReceiverContext} castReceiverContext The cast + * receiver manager. + */ + constructor(namespace, castReceiverContext) { + /** @private @const {string} */ + this.namespace_ = namespace; + /** @private @const {!cast.framework.CastReceiverContext} */ + this.castReceiverContext_ = castReceiverContext; + /** @private @const {!Array} */ + this.messageQueue_ = []; + /** @private @const {!Object} */ + this.actions_ = {}; + /** @private @const {!Object} */ + this.senderSequences_ = {}; + /** @private @const {function(string, *)} */ + this.jsonStringifyReplacer_ = (key, value) => { + if (value === Infinity || value === null) { + return undefined; + } + return value; + }; + this.castReceiverContext_.addCustomMessageListener( + this.namespace_, this.onMessage.bind(this)); + } + + /** + * Registers a handler of a given action. + * + * @param {string} method The method name for which to register the handler. + * @param {!Array>} argDefs The name and type of each argument + * or an empty array if the method has no arguments. + * @param {!ActionHandler} handler A function to process the action. + */ + registerActionHandler(method, argDefs, handler) { + this.actions_[method] = { + method, + argDefs, + handler, + }; + } + + /** + * Unregisters the handler of the given action. + * + * @param {string} action The action to unregister. + */ + unregisterActionHandler(action) { + delete this.actions_[action]; + } + + /** + * Callback to receive messages sent by sender apps. + * + * @param {!cast.framework.system.Event} event The event received from the + * sender app. + */ + onMessage(event) { + console.log('message arrived from sender', this.namespace_, event); + const message = /** @type {!ExoCastMessage} */ (event.data); + const action = this.actions_[message.method]; + if (action) { + const args = message.args; + for (let i = 0; i < action.argDefs.length; i++) { + if (!validation.validateProperty( + args, action.argDefs[i][0], action.argDefs[i][1])) { + console.warn('invalid method call', message); + return; + } + } + this.messageQueue_.push({ + senderId: event.senderId, + message: message, + handler: action.handler + }); + if (this.messageQueue_.length === 1) { + this.executeNext(); + } else { + // Do nothing. An action is executing asynchronously and will call + // executeNext when finished. + } + } else { + console.warn('handler of method not found', message); + } + } + + /** + * Executes the next message in the queue. + */ + executeNext() { + if (this.messageQueue_.length === 0) { + return; + } + const head = this.messageQueue_[0]; + const message = head.message; + const senderSequence = message.sequenceNumber; + this.senderSequences_[head.senderId] = senderSequence; + try { + head.handler(message.args, senderSequence, head.senderId, (response) => { + if (response) { + this.send(head.senderId, response); + } + this.shiftPendingMessage_(head); + }); + } catch (e) { + this.shiftPendingMessage_(head); + console.error('error while executing method : ' + message.method, e); + } + } + + /** + * Broadcasts the sender state to all sender apps registered for the + * given message namespace. + * + * @param {!PlayerState} playerState The player state to be sent. + */ + broadcast(playerState) { + this.castReceiverContext_.getSenders().forEach((sender) => { + this.send(sender.id, playerState); + }); + delete playerState.sequenceNumber; + } + + /** + * Sends the PlayerState to the given sender. + * + * @param {string} senderId The id of the sender. + * @param {!PlayerState} playerState The message to send. + */ + send(senderId, playerState) { + playerState.sequenceNumber = this.senderSequences_[senderId] || -1; + this.castReceiverContext_.sendCustomMessage( + this.namespace_, senderId, + // TODO(bachinger) Find a better solution. + JSON.parse(JSON.stringify(playerState, this.jsonStringifyReplacer_))); + } + + /** + * Notifies the message dispatcher that a given sender has disconnected from + * the receiver. + * + * @param {string} senderId The id of the sender. + */ + notifySenderDisconnected(senderId) { + delete this.senderSequences_[senderId]; + } + + /** + * Shifts the pending message and executes the next if any. + * + * @private + * @param {!Message} pendingMessage The pending message. + */ + shiftPendingMessage_(pendingMessage) { + if (pendingMessage === this.messageQueue_[0]) { + this.messageQueue_.shift(); + this.executeNext(); + } + } +} + +/** + * An item in the message queue. + * + * @record + */ +function Message() {} + +/** + * The sender id. + * + * @type {string} + */ +Message.prototype.senderId; + +/** + * The ExoCastMessage sent by the sender app. + * + * @type {!ExoCastMessage} + */ +Message.prototype.message; + +/** + * The handler function handling the message. + * + * @type {!ActionHandler} + */ +Message.prototype.handler; + +exports = MessageDispatcher; diff --git a/cast_receiver_app/app/src/receiver.js b/cast_receiver_app/app/src/receiver.js new file mode 100644 index 0000000000..5e67219e75 --- /dev/null +++ b/cast_receiver_app/app/src/receiver.js @@ -0,0 +1,191 @@ +/** + * 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. + */ + +goog.module('exoplayer.cast.Receiver'); + +const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher'); +const Player = goog.require('exoplayer.cast.Player'); +const validation = goog.require('exoplayer.cast.validation'); + +/** + * The Receiver receives messages from a message bus and delegates to + * the player. + * + * @constructor + * @param {!Player} player The player. + * @param {!cast.framework.CastReceiverContext} context The cast receiver + * context. + * @param {!MessageDispatcher} messageDispatcher The message dispatcher to use. + */ +const Receiver = function(player, context, messageDispatcher) { + addPlayerActions(messageDispatcher, player); + addQueueActions(messageDispatcher, player); + player.addPlayerListener((playerState) => { + messageDispatcher.broadcast(playerState); + }); + + context.addEventListener( + cast.framework.system.EventType.SENDER_CONNECTED, (event) => { + messageDispatcher.send(event.senderId, player.getPlayerState()); + }); + + context.addEventListener( + cast.framework.system.EventType.SENDER_DISCONNECTED, (event) => { + messageDispatcher.notifySenderDisconnected(event.senderId); + if (event.reason === + cast.framework.system.DisconnectReason.REQUESTED_BY_SENDER && + context.getSenders().length === 0) { + window.close(); + } + }); + + // Start the cast receiver context. + context.start(); +}; + +/** + * Registers action handlers for playback messages sent by the sender app. + * + * @param {!MessageDispatcher} messageDispatcher The dispatcher. + * @param {!Player} player The player. + */ +const addPlayerActions = function(messageDispatcher, player) { + messageDispatcher.registerActionHandler( + 'player.setPlayWhenReady', [['playWhenReady', 'boolean']], + (args, senderSequence, senderId, callback) => { + const playWhenReady = args['playWhenReady']; + callback( + !player.setPlayWhenReady(playWhenReady) ? + player.getPlayerState() : + null); + }); + messageDispatcher.registerActionHandler( + 'player.seekTo', + [ + ['uuid', 'string'], + ['positionMs', '?number'], + ], + (args, senderSequence, senderId, callback) => { + callback( + !player.seekToUuid(args['uuid'], args['positionMs']) ? + player.getPlayerState() : + null); + }); + messageDispatcher.registerActionHandler( + 'player.setRepeatMode', [['repeatMode', 'RepeatMode']], + (args, senderSequence, senderId, callback) => { + callback( + !player.setRepeatMode(args['repeatMode']) ? + player.getPlayerState() : + null); + }); + messageDispatcher.registerActionHandler( + 'player.setShuffleModeEnabled', [['shuffleModeEnabled', 'boolean']], + (args, senderSequence, senderId, callback) => { + callback( + !player.setShuffleModeEnabled(args['shuffleModeEnabled']) ? + player.getPlayerState() : + null); + }); + messageDispatcher.registerActionHandler( + 'player.onClientConnected', [], + (args, senderSequence, senderId, callback) => { + callback(player.getPlayerState()); + }); + messageDispatcher.registerActionHandler( + 'player.stop', [['reset', 'boolean']], + (args, senderSequence, senderId, callback) => { + player.stop(args['reset']).then(() => { + callback(null); + }); + }); + messageDispatcher.registerActionHandler( + 'player.prepare', [], (args, senderSequence, senderId, callback) => { + player.prepare(); + callback(null); + }); + messageDispatcher.registerActionHandler( + 'player.setTrackSelectionParameters', + [ + ['preferredAudioLanguage', 'string'], + ['preferredTextLanguage', 'string'], + ['disabledTextTrackSelectionFlags', 'Array'], + ['selectUndeterminedTextLanguage', 'boolean'], + ], + (args, senderSequence, senderId, callback) => { + const trackSelectionParameters = + /** @type {!TrackSelectionParameters} */ ({ + preferredAudioLanguage: args['preferredAudioLanguage'], + preferredTextLanguage: args['preferredTextLanguage'], + disabledTextTrackSelectionFlags: + args['disabledTextTrackSelectionFlags'], + selectUndeterminedTextLanguage: + args['selectUndeterminedTextLanguage'], + }); + callback( + !player.setTrackSelectionParameters(trackSelectionParameters) ? + player.getPlayerState() : + null); + }); +}; + +/** + * Registers action handlers for queue management messages sent by the sender + * app. + * + * @param {!MessageDispatcher} messageDispatcher The dispatcher. + * @param {!Player} player The player. + */ +const addQueueActions = + function (messageDispatcher, player) { + messageDispatcher.registerActionHandler( + 'player.addItems', + [ + ['index', '?number'], + ['items', 'Array'], + ['shuffleOrder', 'Array'], + ], + (args, senderSequence, senderId, callback) => { + const mediaItems = args['items']; + const index = args['index'] || player.getQueueSize(); + let addedItemCount; + if (validation.validateMediaItems(mediaItems)) { + addedItemCount = + player.addQueueItems(index, mediaItems, args['shuffleOrder']); + } + callback(addedItemCount === 0 ? player.getPlayerState() : null); + }); + messageDispatcher.registerActionHandler( + 'player.removeItems', [['uuids', 'Array']], + (args, senderSequence, senderId, callback) => { + const removedItemsCount = player.removeQueueItems(args['uuids']); + callback(removedItemsCount === 0 ? player.getPlayerState() : null); + }); + messageDispatcher.registerActionHandler( + 'player.moveItem', + [ + ['uuid', 'string'], + ['index', 'number'], + ['shuffleOrder', 'Array'], + ], + (args, senderSequence, senderId, callback) => { + const hasMoved = player.moveQueueItem( + args['uuid'], args['index'], args['shuffleOrder']); + callback(!hasMoved ? player.getPlayerState() : null); + }); +}; + +exports = Receiver; diff --git a/cast_receiver_app/app/src/validation.js b/cast_receiver_app/app/src/validation.js new file mode 100644 index 0000000000..23e2708f8e --- /dev/null +++ b/cast_receiver_app/app/src/validation.js @@ -0,0 +1,163 @@ +/** + * 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. + * + * @fileoverview A validator for messages received from sender apps. + */ + +goog.module('exoplayer.cast.validation'); + +const {getPlaybackType, PlaybackType, RepeatMode} = goog.require('exoplayer.cast.constants'); + +/** + * Media item fields. + * + * @enum {string} + */ +const MediaItemField = { + UUID: 'uuid', + MEDIA: 'media', + MIME_TYPE: 'mimeType', + DRM_SCHEMES: 'drmSchemes', + TITLE: 'title', + DESCRIPTION: 'description', + START_POSITION_US: 'startPositionUs', + END_POSITION_US: 'endPositionUs', +}; + +/** + * DrmScheme fields. + * + * @enum {string} + */ +const DrmSchemeField = { + UUID: 'uuid', + LICENSE_SERVER_URI: 'licenseServer', +}; + +/** + * UriBundle fields. + * + * @enum {string} + */ +const UriBundleField = { + URI: 'uri', + REQUEST_HEADERS: 'requestHeaders', +}; + +/** + * Validates an array of media items. + * + * @param {!Array} mediaItems An array of media items. + * @return {boolean} true if all media items are valid, otherwise false is + * returned. + */ +const validateMediaItems = function (mediaItems) { + for (let i = 0; i < mediaItems.length; i++) { + if (!validateMediaItem(mediaItems[i])) { + return false; + } + } + return true; +}; + +/** + * Validates a queue item sent to the receiver by a sender app. + * + * @param {!MediaItem} mediaItem The media item. + * @return {boolean} true if the media item is valid, false otherwise. + */ +const validateMediaItem = function (mediaItem) { + // validate minimal properties + if (!validateProperty(mediaItem, MediaItemField.UUID, 'string')) { + console.log('missing mandatory uuid', mediaItem.uuid); + return false; + } + if (!validateProperty(mediaItem.media, UriBundleField.URI, 'string')) { + console.log('missing mandatory', mediaItem.media ? 'uri' : 'media'); + return false; + } + const mimeType = mediaItem.mimeType; + if (!mimeType || getPlaybackType(mimeType) === PlaybackType.UNKNOWN) { + console.log('unsupported mime type:', mimeType); + return false; + } + // validate optional properties + if (goog.isArray(mediaItem.drmSchemes)) { + for (let i = 0; i < mediaItem.drmSchemes.length; i++) { + let drmScheme = mediaItem.drmSchemes[i]; + if (!validateProperty(drmScheme, DrmSchemeField.UUID, 'string') || + !validateProperty( + drmScheme.licenseServer, UriBundleField.URI, 'string')) { + console.log('invalid drm scheme', drmScheme); + return false; + } + } + } + if (!validateProperty(mediaItem, MediaItemField.START_POSITION_US, '?number') + || !validateProperty(mediaItem, MediaItemField.END_POSITION_US, '?number') + || !validateProperty(mediaItem, MediaItemField.TITLE, '?string') + || !validateProperty(mediaItem, MediaItemField.DESCRIPTION, '?string')) { + console.log('invalid type of one of startPositionUs, endPositionUs, title' + + ' or description', mediaItem); + return false; + } + return true; +}; + +/** + * Validates the existence and type of a property. + * + *

      Supported types: number, string, boolean, Array. + *

      Prefix the type with a ? to indicate that the property is optional. + * + * @param {?Object|?MediaItem|?UriBundle} obj The object to validate. + * @param {string} propertyName The name of the property. + * @param {string} type The type of the property. + * @return {boolean} True if valid, false otherwise. + */ +const validateProperty = function (obj, propertyName, type) { + if (typeof obj === 'undefined' || obj === null) { + return false; + } + const isOptional = type.startsWith('?'); + const value = obj[propertyName]; + if (isOptional && typeof value === 'undefined') { + return true; + } + type = isOptional ? type.substring(1) : type; + switch (type) { + case 'string': + return typeof value === 'string' || value instanceof String; + case 'number': + return typeof value === 'number' && isFinite(value); + case 'Array': + return typeof value !== 'undefined' && typeof value === 'object' + && value.constructor === Array; + case 'boolean': + return typeof value === 'boolean'; + case 'RepeatMode': + return value === RepeatMode.OFF || value === RepeatMode.ONE || + value === RepeatMode.ALL; + default: + console.warn('Unsupported type when validating an object property. ' + + 'Supported types are string, number, boolean and Array.', type); + return false; + } +}; + +exports.validateMediaItem = validateMediaItem; +exports.validateMediaItems = validateMediaItems; +exports.validateProperty = validateProperty; + diff --git a/cast_receiver_app/assemble.bazel.sh b/cast_receiver_app/assemble.bazel.sh new file mode 100755 index 0000000000..d2039a5152 --- /dev/null +++ b/cast_receiver_app/assemble.bazel.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# Copyright (C) 2019 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. + +## +# Assembles the html, css and javascript files which have been created by the +# bazel build in a destination directory. + +HTML_DIR=app/html +HTML_DEBUG_DIR=app-desktop/html +BIN=bazel-bin + +function usage { + echo "usage: `basename "$0"` -d=DESTINATION_DIR" +} + +for i in "$@" +do +case $i in + -d=*|--destination=*) + DESTINATION="${i#*=}" + shift # past argument=value + ;; + -h|--help) + usage + exit 0 + ;; + *) + # unknown option + ;; +esac +done + +if [ ! -d "$DESTINATION" ]; then + echo "destination directory '$DESTINATION' is not declared or is not a\ + directory" + usage + exit 1 +fi + +if [ ! -f "$BIN/app.js" ];then + echo "file $BIN/app.js not found. Did you build already with bazel?" + echo "-> # bazel build .. --incompatible_package_name_is_a_function=false" + exit 1 +fi + +if [ ! -f "$BIN/app_desktop.js" ];then + echo "file $BIN/app_desktop.js not found. Did you build already with bazel?" + echo "-> # bazel build .. --incompatible_package_name_is_a_function=false" + exit 1 +fi + +echo "assembling receiver and desktop app in $DESTINATION" +echo "-------" + +# cleaning up asset files in destination directory +FILES=( + app.js + app_desktop.js + app_styles.css + app_desktop_styles.css + index.html + player.html +) +for file in ${FILES[@]}; do + if [ -f $DESTINATION/$file ]; then + echo "deleting $file" + rm -f $DESTINATION/$file + fi +done +echo "-------" + +echo "copy html files to $DESTINATION" +cp $HTML_DIR/index.html $DESTINATION +cp $HTML_DEBUG_DIR/index.html $DESTINATION/player.html +echo "copy javascript files to $DESTINATION" +cp $BIN/app.js $BIN/app_desktop.js $DESTINATION +echo "copy css style to $DESTINATION" +cp $BIN/app_styles.css $BIN/app_desktop_styles.css $DESTINATION +echo "-------" + +echo "done." diff --git a/cast_receiver_app/externs/protocol.js b/cast_receiver_app/externs/protocol.js new file mode 100644 index 0000000000..d6544a6f37 --- /dev/null +++ b/cast_receiver_app/externs/protocol.js @@ -0,0 +1,489 @@ +/* + * 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. + */ + +/** + * @fileoverview Externs for messages sent by a sender app in JSON format. + * + * Fields defined here are prevented from being renamed by the js compiler. + * + * @externs + */ + +/** + * An uri bundle with an uri and request parameters. + * + * @record + */ +class UriBundle { + constructor() { + /** + * The URI. + * + * @type {string} + */ + this.uri; + + /** + * The request headers. + * + * @type {?Object} + */ + this.requestHeaders; + } +} + +/** + * @record + */ +class DrmScheme { + constructor() { + /** + * The DRM UUID. + * + * @type {string} + */ + this.uuid; + + /** + * The license URI. + * + * @type {?UriBundle} + */ + this.licenseServer; + } +} + +/** + * @record + */ +class MediaItem { + constructor() { + /** + * The uuid of the item. + * + * @type {string} + */ + this.uuid; + + /** + * The mime type. + * + * @type {string} + */ + this.mimeType; + + /** + * The media uri bundle. + * + * @type {!UriBundle} + */ + this.media; + + /** + * The DRM schemes. + * + * @type {!Array} + */ + this.drmSchemes; + + /** + * The position to start playback from. + * + * @type {number} + */ + this.startPositionUs; + + /** + * The position at which to end playback. + * + * @type {number} + */ + this.endPositionUs; + + /** + * The title of the media item. + * + * @type {string} + */ + this.title; + + /** + * The description of the media item. + * + * @type {string} + */ + this.description; + } +} + +/** + * Constraint parameters for track selection. + * + * @record + */ +class TrackSelectionParameters { + constructor() { + /** + * The preferred audio language. + * + * @type {string|undefined} + */ + this.preferredAudioLanguage; + + /** + * The preferred text language. + * + * @type {string|undefined} + */ + this.preferredTextLanguage; + + /** + * List of selection flags that are disabled for text track selections. + * + * @type {!Array} + */ + this.disabledTextTrackSelectionFlags; + + /** + * Whether a text track with undetermined language should be selected if no + * track with `preferredTextLanguage` is available, or if + * `preferredTextLanguage` is unset. + * + * @type {boolean} + */ + this.selectUndeterminedTextLanguage; + } +} + +/** + * The PlaybackPosition defined by the position, the uuid of the media item and + * the period id. + * + * @record + */ +class PlaybackPosition { + constructor() { + /** + * The current playback position in milliseconds. + * + * @type {number} + */ + this.positionMs; + + /** + * The uuid of the media item. + * + * @type {string} + */ + this.uuid; + + /** + * The id of the currently playing period. + * + * @type {string} + */ + this.periodId; + + /** + * The reason of a position discontinuity if any. + * + * @type {?string} + */ + this.discontinuityReason; + } +} + +/** + * The playback parameters. + * + * @record + */ +class PlaybackParameters { + constructor() { + /** + * The playback speed. + * + * @type {number} + */ + this.speed; + + /** + * The playback pitch. + * + * @type {number} + */ + this.pitch; + + /** + * Whether silence is skipped. + * + * @type {boolean} + */ + this.skipSilence; + } +} +/** + * The player state. + * + * @record + */ +class PlayerState { + constructor() { + /** + * The playback state. + * + * @type {string} + */ + this.playbackState; + + /** + * The playback parameters. + * + * @type {!PlaybackParameters} + */ + this.playbackParameters; + + /** + * Playback starts when ready if true. + * + * @type {boolean} + */ + this.playWhenReady; + + /** + * The current position within the media. + * + * @type {?PlaybackPosition} + */ + this.playbackPosition; + + /** + * The current window index. + * + * @type {number} + */ + this.windowIndex; + + /** + * The number of windows. + * + * @type {number} + */ + this.windowCount; + + /** + * The audio tracks. + * + * @type {!Array} + */ + this.audioTracks; + + /** + * The video tracks in case of adaptive media. + * + * @type {!Array>} + */ + this.videoTracks; + + /** + * The repeat mode. + * + * @type {string} + */ + this.repeatMode; + + /** + * Whether the shuffle mode is enabled. + * + * @type {boolean} + */ + this.shuffleModeEnabled; + + /** + * The playback order to use when shuffle mode is enabled. + * + * @type {!Array} + */ + this.shuffleOrder; + + /** + * The queue of media items. + * + * @type {!Array} + */ + this.mediaQueue; + + /** + * The media item info of the queue items if available. + * + * @type {!Object} + */ + this.mediaItemsInfo; + + /** + * The sequence number of the sender. + * + * @type {number} + */ + this.sequenceNumber; + + /** + * The player error. + * + * @type {?PlayerError} + */ + this.error; + } +} + +/** + * The error description. + * + * @record + */ +class PlayerError { + constructor() { + /** + * The error message. + * + * @type {string} + */ + this.message; + + /** + * The error code. + * + * @type {number} + */ + this.code; + + /** + * The error category. + * + * @type {number} + */ + this.category; + } +} + +/** + * A period. + * + * @record + */ +class Period { + constructor() { + /** + * The id of the period. Must be unique within a media item. + * + * @type {string} + */ + this.id; + + /** + * The duration of the period in microseconds. + * + * @type {number} + */ + this.durationUs; + } +} +/** + * Holds dynamic information for a MediaItem. + * + *

      Holds information related to preparation for a specific {@link MediaItem}. + * Unprepared items are associated with an {@link #EMPTY} info object until + * prepared. + * + * @record + */ +class MediaItemInfo { + constructor() { + /** + * The duration of the window in microseconds. + * + * @type {number} + */ + this.windowDurationUs; + + /** + * The default start position relative to the start of the window in + * microseconds. + * + * @type {number} + */ + this.defaultStartPositionUs; + + /** + * The periods conforming the media item. + * + * @type {!Array} + */ + this.periods; + + /** + * The position of the window in the first period in microseconds. + * + * @type {number} + */ + this.positionInFirstPeriodUs; + + /** + * Whether it is possible to seek within the window. + * + * @type {boolean} + */ + this.isSeekable; + + /** + * Whether the window may change when the timeline is updated. + * + * @type {boolean} + */ + this.isDynamic; + } +} + +/** + * The message envelope send by a sender app. + * + * @record + */ +class ExoCastMessage { + constructor() { + /** + * The clients message sequenec number. + * + * @type {number} + */ + this.sequenceNumber; + + /** + * The name of the method. + * + * @type {string} + */ + this.method; + + /** + * The arguments of the method. + * + * @type {!Object} + */ + this.args; + } +}; + diff --git a/cast_receiver_app/externs/shaka.js b/cast_receiver_app/externs/shaka.js new file mode 100644 index 0000000000..0af36d7b8c --- /dev/null +++ b/cast_receiver_app/externs/shaka.js @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2019 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. + */ + +/** + * @fileoverview Externs of the Shaka configuration. + * + * @externs + */ + +/** + * The drm configuration for the Shaka player. + * + * @record + */ +class DrmConfiguration { + constructor() { + /** + * A map of license servers with the UUID of the drm system as the key and the + * license uri as the value. + * + * @type {!Object} + */ + this.servers; + } +} + +/** + * The configuration of the Shaka player. + * + * @record + */ +class PlayerConfiguration { + constructor() { + /** + * The preferred audio language. + * + * @type {string} + */ + this.preferredAudioLanguage; + + /** + * The preferred text language. + * + * @type {string} + */ + this.preferredTextLanguage; + + /** + * The drm configuration. + * + * @type {?DrmConfiguration} + */ + this.drm; + } +} diff --git a/cast_receiver_app/src/configuration_factory.js b/cast_receiver_app/src/configuration_factory.js new file mode 100644 index 0000000000..819e52a755 --- /dev/null +++ b/cast_receiver_app/src/configuration_factory.js @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2019 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. + */ + +goog.module('exoplayer.cast.ConfigurationFactory'); + +const {DRM_SYSTEMS} = goog.require('exoplayer.cast.constants'); + +const EMPTY_DRM_CONFIGURATION = + /** @type {!DrmConfiguration} */ (Object.freeze({ + servers: {}, + })); + +/** + * Creates the configuration of the Shaka player. + */ +class ConfigurationFactory { + /** + * Creates the Shaka player configuration. + * + * @param {!MediaItem} mediaItem The media item for which to create the + * configuration. + * @param {!TrackSelectionParameters} trackSelectionParameters The track + * selection parameters. + * @return {!PlayerConfiguration} The shaka player configuration. + */ + createConfiguration(mediaItem, trackSelectionParameters) { + const configuration = /** @type {!PlayerConfiguration} */ ({}); + this.mapLanguageConfiguration(trackSelectionParameters, configuration); + this.mapDrmConfiguration_(mediaItem, configuration); + return configuration; + } + + /** + * Maps the preferred audio and text language from the track selection + * parameters to the configuration. + * + * @param {!TrackSelectionParameters} trackSelectionParameters The selection + * parameters. + * @param {!PlayerConfiguration} playerConfiguration The player configuration. + */ + mapLanguageConfiguration(trackSelectionParameters, playerConfiguration) { + playerConfiguration.preferredAudioLanguage = + trackSelectionParameters.preferredAudioLanguage || ''; + playerConfiguration.preferredTextLanguage = + trackSelectionParameters.preferredTextLanguage || ''; + } + + /** + * Maps the drm configuration from the media item to the configuration. If no + * drm is specified for the given media item, null is assigned. + * + * @private + * @param {!MediaItem} mediaItem The media item. + * @param {!PlayerConfiguration} playerConfiguration The player configuration. + */ + mapDrmConfiguration_(mediaItem, playerConfiguration) { + if (!mediaItem.drmSchemes) { + playerConfiguration.drm = EMPTY_DRM_CONFIGURATION; + return; + } + const drmConfiguration = /** @type {!DrmConfiguration} */({ + servers: {}, + }); + let hasDrmServer = false; + mediaItem.drmSchemes.forEach((scheme) => { + const drmSystem = DRM_SYSTEMS[scheme.uuid]; + if (drmSystem && scheme.licenseServer && scheme.licenseServer.uri) { + hasDrmServer = true; + drmConfiguration.servers[drmSystem] = scheme.licenseServer.uri; + } + }); + playerConfiguration.drm = + hasDrmServer ? drmConfiguration : EMPTY_DRM_CONFIGURATION; + } +} + +exports = ConfigurationFactory; diff --git a/cast_receiver_app/src/constants.js b/cast_receiver_app/src/constants.js new file mode 100644 index 0000000000..e9600429f0 --- /dev/null +++ b/cast_receiver_app/src/constants.js @@ -0,0 +1,140 @@ +/** + * Copyright (C) 2019 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. + */ + +goog.module('exoplayer.cast.constants'); + +/** + * The underyling player. + * + * @enum {number} + */ +const PlaybackType = { + VIDEO_ELEMENT: 1, + SHAKA_PLAYER: 2, + UNKNOWN: 999, +}; + +/** + * Supported mime types and their playback mode. + * + * @type {!Object} + */ +const SUPPORTED_MIME_TYPES = Object.freeze({ + 'application/dash+xml': PlaybackType.SHAKA_PLAYER, + 'application/vnd.apple.mpegurl': PlaybackType.SHAKA_PLAYER, + 'application/vnd.ms-sstr+xml': PlaybackType.SHAKA_PLAYER, + 'application/x-mpegURL': PlaybackType.SHAKA_PLAYER, +}); + +/** + * Returns the playback type required for a given mime type, or + * PlaybackType.UNKNOWN if the mime type is not recognized. + * + * @param {string} mimeType The mime type. + * @return {!PlaybackType} The required playback type, or PlaybackType.UNKNOWN + * if the mime type is not recognized. + */ +const getPlaybackType = function(mimeType) { + if (mimeType.startsWith('video/') || mimeType.startsWith('audio/')) { + return PlaybackType.VIDEO_ELEMENT; + } else { + return SUPPORTED_MIME_TYPES[mimeType] || PlaybackType.UNKNOWN; + } +}; + +/** + * Error messages. + * + * @enum {string} + */ +const ErrorMessages = { + SHAKA_LOAD_ERROR: 'Error while loading media with Shaka.', + SHAKA_UNKNOWN_ERROR: 'Shaka error event captured.', + MEDIA_ELEMENT_UNKNOWN_ERROR: 'Media element error event captured.', + UNKNOWN_FATAL_ERROR: 'Fatal playback error. Shaka instance replaced.', + UNKNOWN_ERROR: 'Unknown error', +}; + +/** + * ExoPlayer's repeat modes. + * + * @enum {string} + */ +const RepeatMode = { + OFF: 'OFF', + ONE: 'ONE', + ALL: 'ALL', +}; + +/** + * Error categories. Error categories coming from Shaka are defined in [Shaka + * source + * code](https://shaka-player-demo.appspot.com/docs/api/shaka.util.Error.html). + * + * @enum {number} + */ +const ErrorCategory = { + MEDIA_ELEMENT: 0, + FATAL_SHAKA_ERROR: 1000, +}; + +/** + * An error object to be used if no media error is assigned to the `error` + * field of the media element when an error event is fired + * + * @type {!PlayerError} + */ +const UNKNOWN_ERROR = /** @type {!PlayerError} */ (Object.freeze({ + message: ErrorMessages.UNKNOWN_ERROR, + code: 0, + category: 0, +})); + +/** + * UUID for the Widevine DRM scheme. + * + * @type {string} + */ +const WIDEVINE_UUID = 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'; + +/** + * UUID for the PlayReady DRM scheme. + * + * @type {string} + */ +const PLAYREADY_UUID = '9a04f079-9840-4286-ab92-e65be0885f95'; + +/** @type {!Object} */ +const drmSystems = {}; +drmSystems[WIDEVINE_UUID] = 'com.widevine.alpha'; +drmSystems[PLAYREADY_UUID] = 'com.microsoft.playready'; + +/** + * The uuids of the supported DRM systems. + * + * @type {!Object} + */ +const DRM_SYSTEMS = Object.freeze(drmSystems); + +exports.PlaybackType = PlaybackType; +exports.ErrorMessages = ErrorMessages; +exports.ErrorCategory = ErrorCategory; +exports.RepeatMode = RepeatMode; +exports.getPlaybackType = getPlaybackType; +exports.WIDEVINE_UUID = WIDEVINE_UUID; +exports.PLAYREADY_UUID = PLAYREADY_UUID; +exports.DRM_SYSTEMS = DRM_SYSTEMS; +exports.UNKNOWN_ERROR = UNKNOWN_ERROR; diff --git a/cast_receiver_app/src/playback_info_view.js b/cast_receiver_app/src/playback_info_view.js new file mode 100644 index 0000000000..22e2b8ded5 --- /dev/null +++ b/cast_receiver_app/src/playback_info_view.js @@ -0,0 +1,233 @@ +/* + * 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. + */ + +goog.module('exoplayer.cast.PlaybackInfoView'); + +const Player = goog.require('exoplayer.cast.Player'); +const Timeout = goog.require('exoplayer.cast.Timeout'); +const dom = goog.require('goog.dom'); + +/** The default timeout for hiding the UI in milliseconds. */ +const SHOW_TIMEOUT_MS = 5000; +/** The timeout for hiding the UI in audio only mode in milliseconds. */ +const SHOW_TIMEOUT_MS_AUDIO = 0; +/** The timeout for updating the UI while being displayed. */ +const UPDATE_TIMEOUT_MS = 1000; + +/** + * Formats a duration in milliseconds to a string in hh:mm:ss format. + * + * @param {number} durationMs The duration in milliseconds. + * @return {string} The duration formatted as hh:mm:ss. + */ +const formatTimestampMsAsString = function (durationMs) { + const hours = Math.floor(durationMs / 1000 / 60 / 60); + const minutes = Math.floor((durationMs / 1000 / 60) % 60); + const seconds = Math.floor((durationMs / 1000) % 60) % 60; + let timeString = ''; + if (hours > 0) { + timeString += hours + ':'; + } + if (minutes < 10) { + timeString += '0'; + } + timeString += minutes + ":"; + if (seconds < 10) { + timeString += '0'; + } + timeString += seconds; + return timeString; +}; + +/** + * A view to display information about the current media item and playback + * progress. + * + * @constructor + * @param {!Player} player The player of which to display the + * playback info. + * @param {string} viewId The id of the playback info view. + */ +const PlaybackInfoView = function (player, viewId) { + /** @const @private {!Player} */ + this.player_ = player; + /** @const @private {?Element} */ + this.container_ = document.getElementById(viewId); + /** @const @private {?Element} */ + this.elapsedTimeBar_ = document.getElementById('exo_elapsed_time'); + /** @const @private {?Element} */ + this.elapsedTimeLabel_ = document.getElementById('exo_elapsed_time_label'); + /** @const @private {?Element} */ + this.durationLabel_ = document.getElementById('exo_duration_label'); + /** @const @private {!Timeout} */ + this.hideTimeout_ = new Timeout(); + /** @const @private {!Timeout} */ + this.updateTimeout_ = new Timeout(); + /** @private {boolean} */ + this.wasPlaying_ = player.getPlayWhenReady() + && player.getPlaybackState() === Player.PlaybackState.READY; + /** @private {number} */ + this.showTimeoutMs_ = SHOW_TIMEOUT_MS; + /** @private {number} */ + this.showTimeoutMsVideo_ = this.showTimeoutMs_; + + if (this.wasPlaying_) { + this.hideAfterTimeout(); + } else { + this.show(); + } + + player.addPlayerListener((playerState) => { + if (this.container_ === null) { + return; + } + const playbackPosition = playerState.playbackPosition; + const discontinuityReason = + playbackPosition ? playbackPosition.discontinuityReason : null; + if (discontinuityReason) { + const currentMediaItem = player.getCurrentMediaItem(); + this.showTimeoutMs_ = + currentMediaItem && currentMediaItem.mimeType === 'audio/*' ? + SHOW_TIMEOUT_MS_AUDIO : + this.showTimeoutMsVideo_; + } + const playWhenReady = playerState.playWhenReady; + const state = playerState.playbackState; + const isPlaying = playWhenReady && state === Player.PlaybackState.READY; + const userSeekedInBufferedRange = + discontinuityReason === Player.DiscontinuityReason.SEEK && isPlaying; + if (!isPlaying) { + this.show(); + } else if ((!this.wasPlaying_ && isPlaying) || userSeekedInBufferedRange) { + this.hideAfterTimeout(); + } + this.wasPlaying_ = isPlaying; + }); +}; + +/** Shows the player info view. */ +PlaybackInfoView.prototype.show = function () { + if (this.container_ != null) { + this.hideTimeout_.cancel(); + this.updateUi_(); + this.container_.style.display = 'block'; + this.startUpdateTimeout_(); + } +}; + +/** Hides the player info view. */ +PlaybackInfoView.prototype.hideAfterTimeout = function() { + if (this.container_ === null) { + return; + } + this.show(); + this.hideTimeout_.postDelayed(this.showTimeoutMs_).then(() => { + this.container_.style.display = 'none'; + this.updateTimeout_.cancel(); + }); +}; + +/** + * Sets the playback info view timeout. The playback info view is automatically + * hidden after this duration of time has elapsed without show() being called + * again. When playing streams with content type 'audio/*' the view is always + * displayed. + * + * @param {number} showTimeoutMs The duration in milliseconds. A non-positive + * value will cause the view to remain visible indefinitely. + */ +PlaybackInfoView.prototype.setShowTimeoutMs = function(showTimeoutMs) { + this.showTimeoutMs_ = showTimeoutMs; + this.showTimeoutMsVideo_ = showTimeoutMs; +}; + +/** + * Updates all UI components. + * + * @private + */ +PlaybackInfoView.prototype.updateUi_ = function () { + const elapsedTimeMs = this.player_.getCurrentPositionMs(); + const durationMs = this.player_.getDurationMs(); + if (this.elapsedTimeLabel_ !== null) { + this.updateDuration_(this.elapsedTimeLabel_, elapsedTimeMs, false); + } + if (this.durationLabel_ !== null) { + this.updateDuration_(this.durationLabel_, durationMs, true); + } + if (this.elapsedTimeBar_ !== null) { + this.updateProgressBar_(elapsedTimeMs, durationMs); + } +}; + +/** + * Adjust the progress bar indicating the elapsed time relative to the duration. + * + * @private + * @param {number} elapsedTimeMs The elapsed time in milliseconds. + * @param {number} durationMs The duration in milliseconds. + */ +PlaybackInfoView.prototype.updateProgressBar_ = + function(elapsedTimeMs, durationMs) { + if (elapsedTimeMs <= 0 || durationMs <= 0) { + this.elapsedTimeBar_.style.width = 0; + } else { + const widthPercentage = elapsedTimeMs / durationMs * 100; + this.elapsedTimeBar_.style.width = Math.min(100, widthPercentage) + '%'; + } +}; + +/** + * Updates the display value of the duration in the DOM formatted as hh:mm:ss. + * + * @private + * @param {!Element} element The element to update. + * @param {number} durationMs The duration in milliseconds. + * @param {boolean} hideZero If true values of zero and below are not displayed. + */ +PlaybackInfoView.prototype.updateDuration_ = + function (element, durationMs, hideZero) { + while (element.firstChild) { + element.removeChild(element.firstChild); + } + if (durationMs <= 0 && !hideZero) { + element.appendChild(dom.createDom(dom.TagName.SPAN, {}, + formatTimestampMsAsString(0))); + } else if (durationMs > 0) { + element.appendChild(dom.createDom(dom.TagName.SPAN, {}, + formatTimestampMsAsString(durationMs))); + } +}; + +/** + * Starts a repeating timeout that updates the UI every UPDATE_TIMEOUT_MS + * milliseconds. + * + * @private + */ +PlaybackInfoView.prototype.startUpdateTimeout_ = function() { + this.updateTimeout_.cancel(); + if (!this.player_.getPlayWhenReady() || + this.player_.getPlaybackState() !== Player.PlaybackState.READY) { + return; + } + this.updateTimeout_.postDelayed(UPDATE_TIMEOUT_MS).then(() => { + this.updateUi_(); + this.startUpdateTimeout_(); + }); +}; + +exports = PlaybackInfoView; diff --git a/cast_receiver_app/src/player.js b/cast_receiver_app/src/player.js new file mode 100644 index 0000000000..d7ffc58f4c --- /dev/null +++ b/cast_receiver_app/src/player.js @@ -0,0 +1,1522 @@ +/* + * 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. + */ + +goog.module('exoplayer.cast.Player'); + +const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); +const NetworkingEngine = goog.require('shaka.net.NetworkingEngine'); +const ShakaError = goog.require('shaka.util.Error'); +const ShakaPlayer = goog.require('shaka.Player'); +const asserts = goog.require('goog.dom.asserts'); +const googArray = goog.require('goog.array'); +const safedom = goog.require('goog.dom.safe'); +const {ErrorMessages, ErrorCategory, PlaybackType, RepeatMode, getPlaybackType, UNKNOWN_ERROR} = goog.require('exoplayer.cast.constants'); +const {UuidComparator, createUuidComparator, log} = goog.require('exoplayer.cast.util'); +const {assert, fail} = goog.require('goog.asserts'); +const {clamp} = goog.require('goog.math'); + +/** + * Value indicating that no window index is currently set. + */ +const INDEX_UNSET = -1; + +/** + * Estimated time for processing the manifest after download in millisecconds. + * + * See: https://github.com/google/shaka-player/issues/1734 + */ +const MANIFEST_PROCESSING_ESTIMATE_MS = 350; + +/** + * Media element events to listen to. + * + * @enum {string} + */ +const MediaElementEvent = { + ERROR: 'error', + LOADED_DATA: 'loadeddata', + PAUSE: 'pause', + PLAYING: 'playing', + SEEKED: 'seeked', + SEEKING: 'seeking', + WAITING: 'waiting', +}; + +/** + * Shaka events to listen to. + * + * @enum {string} + */ +const ShakaEvent = { + ERROR: 'error', + STREAMING: 'streaming', + TRACKS_CHANGED: 'trackschanged', +}; + +/** + * ExoPlayer's playback states. + * + * @enum {string} + */ +const PlaybackState = { + IDLE: 'IDLE', + BUFFERING: 'BUFFERING', + READY: 'READY', + ENDED: 'ENDED', +}; + +/** + * ExoPlayer's position discontinuity reasons. + * + * @enum {string} + */ +const DiscontinuityReason = { + PERIOD_TRANSITION: 'PERIOD_TRANSITION', + SEEK: 'SEEK', +}; + +/** + * A dummy `MediaIteminfo` to be used while the actual period is not + * yet available. + * + * @const + * @type {!MediaItemInfo} + */ +const DUMMY_MEDIA_ITEM_INFO = Object.freeze({ + isSeekable: false, + isDynamic: true, + positionInFirstPeriodUs: 0, + defaultStartPositionUs: 0, + windowDurationUs: 0, + periods: [{ + id: 1, + durationUs: 0, + }], +}); + +/** + * The Player wraps a Shaka player and maintains a queue of media items. + * + * After construction the player is in `IDLE` state. Calling `#prepare` prepares + * the player with the queue item at the given window index and position. The + * state transitions to `BUFFERING`. When 'playWhenReady' is set to `true` + * playback start when the player becomes 'READY'. + * + * When the player needs to rebuffer the state goes to 'BUFFERING' and becomes + * 'READY' again when playback can be resumed. + * + * The state transitions to `ENDED` when playback reached the end of the last + * item in the queue, when the last item has been removed from the queue if + * `!IDLE`, or when `prepare` is called with an empty queue. Seeking makes the + * player transition away from `ENDED` again. + * + * When `#stop` is called or when a fatal playback error occurs, the player + * transition to `IDLE` state and needs to be prepared again to resume playback. + * + * `playWhenReady`, `repeatMode`, `shuffleModeEnabled` can be manipulated in any + * state, just as media items can be added, moved and removed. + * + * @constructor + * @param {!ShakaPlayer} shakaPlayer The shaka player to wrap. + * @param {!ConfigurationFactory} configurationFactory A factory to create a + * configuration for the Shaka player. + */ +const Player = function(shakaPlayer, configurationFactory) { + /** @private @const {?HTMLMediaElement} */ + this.videoElement_ = shakaPlayer.getMediaElement(); + /** @private @const {!ConfigurationFactory} */ + this.configurationFactory_ = configurationFactory; + /** @private @const {!Array} */ + this.playerListeners_ = []; + /** + * @private + * @const + * {?function(NetworkingEngine.RequestType, (?|null))} + */ + this.manifestResponseFilter_ = (type, response) => { + if (type === NetworkingEngine.RequestType.MANIFEST) { + setTimeout(() => { + this.updateWindowMediaItemInfo_(); + this.invalidate(); + }, MANIFEST_PROCESSING_ESTIMATE_MS); + } + }; + + /** @private {!ShakaPlayer} */ + this.shakaPlayer_ = shakaPlayer; + /** @private {boolean} */ + this.playWhenReady_ = false; + /** @private {boolean} */ + this.shuffleModeEnabled_ = false; + /** @private {!RepeatMode} */ + this.repeatMode_ = RepeatMode.OFF; + /** @private {!TrackSelectionParameters} */ + this.trackSelectionParameters_ = /** @type {!TrackSelectionParameters} */ ({ + preferredAudioLanguage: '', + preferredTextLanguage: '', + disabledTextTrackSelectionFlags: [], + selectUndeterminedTextLanguage: false, + }); + /** @private {number} */ + this.windowIndex_ = INDEX_UNSET; + /** @private {!Array} */ + this.queue_ = []; + /** @private {!Object} */ + this.queueUuidIndexMap_ = {}; + /** @private {!UuidComparator} */ + this.uuidComparator_ = createUuidComparator(this.queueUuidIndexMap_); + + /** @private {!PlaybackState} */ + this.playbackState_ = PlaybackState.IDLE; + /** @private {!MediaItemInfo} */ + this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; + /** @private {number} */ + this.windowPeriodIndex_ = 0; + /** @private {!Object} */ + this.mediaItemInfoMap_ = {}; + /** @private {?PlayerError} */ + this.playbackError_ = null; + /** @private {?DiscontinuityReason} */ + this.discontinuityReason_ = null; + /** @private {!Array} */ + this.shuffleOrder_ = []; + /** @private {number} */ + this.shuffleIndex_ = 0; + /** @private {!PlaybackType} */ + this.playbackType_ = PlaybackType.UNKNOWN; + /** @private {boolean} */ + this.isManifestFilterRegistered_ = false; + /** @private {?string} */ + this.uuidToPrepare_ = null; + + if (!this.shakaPlayer_ || !this.videoElement_) { + throw new Error('an instance of Shaka player with a media element ' + + 'attached to it needs to be passed to the constructor.'); + } + + /** @private @const {function(!Event)} */ + this.playbackStateListener_ = (ev) => { + log(['handle event: ', ev.type]); + let invalid = false; + switch (ev.type) { + case ShakaEvent.STREAMING: { + // Arrives once after prepare when the manifest is available. + const uuid = this.queue_[this.windowIndex_].uuid; + const cachedMediaItemInfo = this.mediaItemInfoMap_[uuid]; + if (!cachedMediaItemInfo || cachedMediaItemInfo.isDynamic) { + this.updateWindowMediaItemInfo_(); + if (this.windowMediaItemInfo_.isDynamic) { + this.registerManifestResponseFilter_(); + } + invalid = true; + } + break; + } + case ShakaEvent.TRACKS_CHANGED: { + // Arrives when tracks have changed either initially or at a period + // boundary. + const periods = this.windowMediaItemInfo_.periods; + const previousPeriodIndex = this.windowPeriodIndex_; + this.evaluateAndSetCurrentPeriod_(periods); + invalid = previousPeriodIndex !== this.windowPeriodIndex_; + if (periods.length && this.windowPeriodIndex_ > 0) { + // Player transitions to next period in multiperiod stream. + this.discontinuityReason_ = this.discontinuityReason_ || + DiscontinuityReason.PERIOD_TRANSITION; + invalid = true; + } + if (this.videoElement_.paused && this.playWhenReady_) { + this.videoElement_.play(); + } + break; + } + case MediaElementEvent.LOADED_DATA: { + // Arrives once when the first frame has been rendered. + if (this.playbackType_ === PlaybackType.VIDEO_ELEMENT) { + const uuid = this.queue_[this.windowIndex_].uuid; + let mediaItemInfo = this.mediaItemInfoMap_[uuid]; + if (!mediaItemInfo || mediaItemInfo.isDynamic) { + mediaItemInfo = this.buildMediaItemInfoFromElement_(); + if (mediaItemInfo !== null) { + this.mediaItemInfoMap_[uuid] = mediaItemInfo; + this.windowMediaItemInfo_ = mediaItemInfo; + } + } + this.evaluateAndSetCurrentPeriod_(mediaItemInfo.periods); + invalid = true; + } + if (this.videoElement_.paused && this.playWhenReady_) { + // Restart after automatic skip to next queue item. + this.videoElement_.play(); + } else if (this.videoElement_.paused) { + // If paused, the PLAYING event will not be fired, hence we transition + // to state READY right here. + this.playbackState_ = PlaybackState.READY; + invalid = true; + } + break; + } + case MediaElementEvent.WAITING: + case MediaElementEvent.SEEKING: { + // Arrives at a user seek or when re-buffering starts. + if (this.playbackState_ !== PlaybackState.BUFFERING) { + this.playbackState_ = PlaybackState.BUFFERING; + invalid = true; + } + break; + } + case MediaElementEvent.PLAYING: + case MediaElementEvent.SEEKED: { + // Arrives at the end of a user seek or after re-buffering. + if (this.playbackState_ !== PlaybackState.READY) { + this.playbackState_ = PlaybackState.READY; + invalid = true; + } + break; + } + case MediaElementEvent.PAUSE: { + // Detects end of media and either skips to next or transitions to ended + // state. + if (this.videoElement_.ended) { + let nextWindowIndex = this.getNextWindowIndex(); + if (nextWindowIndex !== INDEX_UNSET) { + this.seekToWindowInternal_(nextWindowIndex, undefined); + } else { + this.playbackState_ = PlaybackState.ENDED; + invalid = true; + } + } + break; + } + } + if (invalid) { + this.invalidate(); + } + }; + /** @private @const {function(!Event)} */ + this.mediaElementErrorHandler_ = (ev) => { + console.error('Media element error reported in handler'); + this.playbackError_ = !this.videoElement_.error ? UNKNOWN_ERROR : { + message: this.videoElement_.error.message, + code: this.videoElement_.error.code, + category: ErrorCategory.MEDIA_ELEMENT, + }; + this.playbackState_ = PlaybackState.IDLE; + this.uuidToPrepare_ = this.queue_[this.windowIndex_] ? + this.queue_[this.windowIndex_].uuid : + null; + this.invalidate(); + }; + /** @private @const {function(!Event)} */ + this.shakaErrorHandler_ = (ev) => { + const shakaError = /** @type {!ShakaError} */ (ev['detail']); + if (shakaError.severity !== ShakaError.Severity.RECOVERABLE) { + this.fatalShakaError_(shakaError, 'Shaka error reported by error event'); + this.invalidate(); + } else { + console.error('Recoverable Shaka error reported in handler'); + } + }; + + this.shakaPlayer_.addEventListener( + ShakaEvent.STREAMING, this.playbackStateListener_); + this.shakaPlayer_.addEventListener( + ShakaEvent.TRACKS_CHANGED, this.playbackStateListener_); + + this.videoElement_.addEventListener( + MediaElementEvent.LOADED_DATA, this.playbackStateListener_); + this.videoElement_.addEventListener( + MediaElementEvent.WAITING, this.playbackStateListener_); + this.videoElement_.addEventListener( + MediaElementEvent.PLAYING, this.playbackStateListener_); + this.videoElement_.addEventListener( + MediaElementEvent.PAUSE, this.playbackStateListener_); + this.videoElement_.addEventListener( + MediaElementEvent.SEEKING, this.playbackStateListener_); + this.videoElement_.addEventListener( + MediaElementEvent.SEEKED, this.playbackStateListener_); + + // Attach error handlers. + this.shakaPlayer_.addEventListener(ShakaEvent.ERROR, this.shakaErrorHandler_); + this.videoElement_.addEventListener( + MediaElementEvent.ERROR, this.mediaElementErrorHandler_); +}; + +/** + * Adds a listener to the player. + * + * @param {function(!PlayerState)} listener The player listener. + */ +Player.prototype.addPlayerListener = function(listener) { + this.playerListeners_.push(listener); +}; + +/** + * Removes a listener. + * + * @param {function(!Object)} listener The player listener. + */ +Player.prototype.removePlayerListener = function(listener) { + for (let i = 0; i < this.playerListeners_.length; i++) { + if (this.playerListeners_[i] === listener) { + this.playerListeners_.splice(i, 1); + break; + } + } +}; + +/** + * Gets the current PlayerState. + * + * @return {!PlayerState} + */ +Player.prototype.getPlayerState = function() { + return this.buildPlayerState_(); +}; + +/** + * Sends the current playback state to clients. + */ +Player.prototype.invalidate = function() { + const playbackState = this.buildPlayerState_(); + for (let i = 0; i < this.playerListeners_.length; i++) { + this.playerListeners_[i](playbackState); + } +}; + +/** + * Get the audio tracks. + * + * @return {!Array} An array with the track names}. + */ +Player.prototype.getAudioTracks = function() { + return this.windowMediaItemInfo_ !== DUMMY_MEDIA_ITEM_INFO ? + this.shakaPlayer_.getAudioLanguages() : + []; +}; + +/** + * Gets the video tracks. + * + * @return {!Array} An array with the video tracks. + */ +Player.prototype.getVideoTracks = function() { + return this.windowMediaItemInfo_ !== DUMMY_MEDIA_ITEM_INFO ? + this.shakaPlayer_.getVariantTracks() : + []; +}; + +/** + * Gets the playback state. + * + * @return {!PlaybackState} The playback state. + */ +Player.prototype.getPlaybackState = function() { + return this.playbackState_; +}; + +/** + * Gets the playback error if any. + * + * @return {?Object} The playback error. + */ +Player.prototype.getPlaybackError = function() { + return this.playbackError_; +}; + +/** + * Gets the duration in milliseconds or a negative value if unknown. + * + * @return {number} The duration in milliseconds. + */ +Player.prototype.getDurationMs = function() { + return this.windowMediaItemInfo_ ? + this.windowMediaItemInfo_.windowDurationUs / 1000 : -1; +}; + +/** + * Gets the current position in milliseconds or a negative value if not known. + * + * @return {number} The current position in milliseconds. + */ +Player.prototype.getCurrentPositionMs = function() { + if (!this.videoElement_.currentTime) { + return 0; + } + return (this.videoElement_.currentTime * 1000) - + (this.windowMediaItemInfo_.positionInFirstPeriodUs / 1000); +}; + +/** + * Gets the current window index. + * + * @return {number} The current window index. + */ +Player.prototype.getCurrentWindowIndex = function() { + if (this.playbackState_ === PlaybackState.IDLE) { + return this.queueUuidIndexMap_[this.uuidToPrepare_ || ''] || 0; + } + return Math.max(0, this.windowIndex_); +}; + +/** + * Gets the media item of the current window or null if the queue is empty. + * + * @return {?MediaItem} The media item of the current window. + */ +Player.prototype.getCurrentMediaItem = function() { + return this.windowIndex_ >= 0 ? this.queue_[this.windowIndex_] : null; +}; + +/** + * Gets the media item info of the current window index or null if not yet + * available. + * + * @return {?MediaItemInfo} The current media item info or undefined. + */ +Player.prototype.getCurrentMediaItemInfo = function () { + return this.windowMediaItemInfo_; +}; + +/** + * Gets the text tracks. + * + * @return {!TextTrackList} The text tracks. + */ +Player.prototype.getTextTracks = function() { + return this.videoElement_.textTracks; +}; + +/** + * Gets whether the player should play when ready. + * + * @return {boolean} True when it plays when ready. + */ +Player.prototype.getPlayWhenReady = function() { + return this.playWhenReady_; +}; + +/** + * Sets whether to play when ready. + * + * @param {boolean} playWhenReady Whether to play when ready. + * @return {boolean} Whether calling this method causes a change of the player + * state. + */ +Player.prototype.setPlayWhenReady = function(playWhenReady) { + if (this.playWhenReady_ === playWhenReady) { + return false; + } + this.playWhenReady_ = playWhenReady; + this.invalidate(); + if (this.playbackState_ === PlaybackState.IDLE || + this.playbackState_ === PlaybackState.ENDED) { + return true; + } + if (this.playWhenReady_) { + this.videoElement_.play(); + } else { + this.videoElement_.pause(); + } + return true; +}; + +/** + * Gets the repeat mode. + * + * @return {!RepeatMode} The repeat mode. + */ +Player.prototype.getRepeatMode = function() { + return this.repeatMode_; +}; + +/** + * Sets the repeat mode. Must be a value of the enum Player.RepeatMode. + * + * @param {!RepeatMode} mode The repeat mode. + * @return {boolean} Whether calling this method causes a change of the player + * state. + */ +Player.prototype.setRepeatMode = function(mode) { + if (this.repeatMode_ === mode) { + return false; + } + if (mode === Player.RepeatMode.OFF || + mode === Player.RepeatMode.ONE || + mode === Player.RepeatMode.ALL) { + this.repeatMode_ = mode; + } else { + throw new Error('illegal repeat mode: ' + mode); + } + this.invalidate(); + return true; +}; + +/** + * Enables or disables the shuffle mode. + * + * @param {boolean} enabled Whether the shuffle mode is enabled or not. + * @return {boolean} Whether calling this method causes a change of the player + * state. + */ +Player.prototype.setShuffleModeEnabled = function(enabled) { + if (this.shuffleModeEnabled_ === enabled) { + return false; + } + this.shuffleModeEnabled_ = enabled; + this.invalidate(); + return true; +}; + +/** + * Sets the track selection parameters. + * + * @param {!TrackSelectionParameters} trackSelectionParameters The parameters. + * @return {boolean} Whether calling this method causes a change of the player + * state. + */ +Player.prototype.setTrackSelectionParameters = function( + trackSelectionParameters) { + this.trackSelectionParameters_ = trackSelectionParameters; + /** @type {!PlayerConfiguration} */ + const configuration = /** @type {!PlayerConfiguration} */ ({}); + this.configurationFactory_.mapLanguageConfiguration( + trackSelectionParameters, configuration); + /** @type {!PlayerConfiguration} */ + const currentConfiguration = this.shakaPlayer_.getConfiguration(); + /** @type {boolean} */ + let isStateChange = false; + if (currentConfiguration.preferredAudioLanguage !== + configuration.preferredAudioLanguage) { + this.shakaPlayer_.selectAudioLanguage(configuration.preferredAudioLanguage); + isStateChange = true; + } + if (currentConfiguration.preferredTextLanguage !== + configuration.preferredTextLanguage) { + this.shakaPlayer_.selectTextLanguage(configuration.preferredTextLanguage); + isStateChange = true; + } + return isStateChange; +}; + +/** + * Gets the previous window index or a negative number if no item previous to + * the current item is available. + * + * @return {number} The previous window index or a negative number if the + * current item is the first item. + */ +Player.prototype.getPreviousWindowIndex = function() { + if (this.playbackType_ === PlaybackType.UNKNOWN) { + return INDEX_UNSET; + } + switch (this.repeatMode_) { + case RepeatMode.ONE: + return this.windowIndex_; + case RepeatMode.ALL: + if (this.shuffleModeEnabled_) { + const previousIndex = this.shuffleIndex_ > 0 ? + this.shuffleIndex_ - 1 : this.queue_.length - 1; + return this.shuffleOrder_[previousIndex]; + } else { + const previousIndex = this.windowIndex_ > 0 ? + this.windowIndex_ - 1 : this.queue_.length - 1; + return previousIndex; + } + break; + case RepeatMode.OFF: + if (this.shuffleModeEnabled_) { + const previousIndex = this.shuffleIndex_ - 1; + return previousIndex < 0 ? -1 : this.shuffleOrder_[previousIndex]; + } else { + const previousIndex = this.windowIndex_ - 1; + return previousIndex < 0 ? -1 : previousIndex; + } + break; + default: + throw new Error('illegal state of repeat mode: ' + this.repeatMode_); + } +}; + +/** + * Gets the next window index or a negative number if the current item is the + * last item. + * + * @return {number} The next window index or a negative number if the current + * item is the last item. + */ +Player.prototype.getNextWindowIndex = function() { + if (this.playbackType_ === PlaybackType.UNKNOWN) { + return INDEX_UNSET; + } + switch (this.repeatMode_) { + case RepeatMode.ONE: + return this.windowIndex_; + case RepeatMode.ALL: + if (this.shuffleModeEnabled_) { + const nextIndex = (this.shuffleIndex_ + 1) % this.queue_.length; + return this.shuffleOrder_[nextIndex]; + } else { + return (this.windowIndex_ + 1) % this.queue_.length; + } + break; + case RepeatMode.OFF: + if (this.shuffleModeEnabled_) { + const nextIndex = this.shuffleIndex_ + 1; + return nextIndex < this.shuffleOrder_.length ? + this.shuffleOrder_[nextIndex] : -1; + } else { + const nextIndex = this.windowIndex_ + 1; + return nextIndex < this.queue_.length ? nextIndex : -1; + } + break; + default: + throw new Error('illegal state of repeat mode: ' + this.repeatMode_); + } +}; + +/** + * Gets whether the current window is seekable. + * + * @return {boolean} True if seekable. + */ +Player.prototype.isCurrentWindowSeekable = function() { + return !!this.videoElement_.seekable; +}; + +/** + * Seeks to the positionMs of the media item with the given uuid. + * + * @param {string} uuid The uuid of the media item to seek to. + * @param {number|undefined} positionMs The position in milliseconds to seek to. + * @return {boolean} True if a seek operation has been processed, false + * otherwise. + */ +Player.prototype.seekToUuid = function(uuid, positionMs) { + if (this.playbackState_ === PlaybackState.IDLE) { + this.uuidToPrepare_ = uuid; + this.videoElement_.currentTime = + this.getPosition_(positionMs, INDEX_UNSET) / 1000; + this.invalidate(); + return true; + } + const windowIndex = this.queueUuidIndexMap_[uuid]; + if (windowIndex !== undefined) { + positionMs = this.getPosition_(positionMs, windowIndex); + this.discontinuityReason_ = DiscontinuityReason.SEEK; + this.seekToWindowInternal_(windowIndex, positionMs); + return true; + } + return false; +}; + +/** + * Seeks to the positionMs of the given window. + * + * The index must be a valid index of the current queue, else this method does + * nothing. + * + * @param {number} windowIndex The index of the window to seek to. + * @param {number|undefined} positionMs The position to seek to within the + * window. + */ +Player.prototype.seekToWindow = function(windowIndex, positionMs) { + if (windowIndex < 0 || windowIndex >= this.queue_.length) { + return; + } + this.seekToUuid(this.queue_[windowIndex].uuid, positionMs); +}; + +/** + * Gets the number of media items in the queue. + * + * @return {number} The size of the queue. + */ +Player.prototype.getQueueSize = function() { + return this.queue_.length; +}; + +/** + * Adds an array of items at the given index of the queue. + * + * Items are expected to have been validated with `validation#validateMediaItem` + * or `validation#validateMediaItems` before being passed to this method. + * + * @param {number} index The index where to insert the media item. + * @param {!Array} mediaItems The media items. + * @param {!Array|undefined} shuffleOrder The new shuffle order. + * @return {number} The number of added items. + */ +Player.prototype.addQueueItems = function(index, mediaItems, shuffleOrder) { + if (index < 0 || mediaItems.length === 0) { + return 0; + } + let addedItemCount = 0; + index = Math.min(this.queue_.length, index); + mediaItems.forEach((itemToAdd) => { + if (this.queueUuidIndexMap_[itemToAdd.uuid] === undefined) { + this.queue_.splice(index + addedItemCount, 0, itemToAdd); + this.queueUuidIndexMap_[itemToAdd.uuid] = index + addedItemCount; + addedItemCount++; + } + }); + if (addedItemCount === 0) { + return 0; + } + this.buildUuidIndexMap_(index + addedItemCount); + this.setShuffleOrder_(shuffleOrder); + if (this.queue_.length === addedItemCount) { + this.windowIndex_ = 0; + this.updateShuffleIndex_(); + } else if ( + index <= this.windowIndex_ && + this.playbackType_ !== PlaybackType.UNKNOWN) { + this.windowIndex_ += mediaItems.length; + this.updateShuffleIndex_(); + } + this.invalidate(); + return addedItemCount; +}; + +/** + * Removes the queue items with the given uuids. + * + * @param {!Array} uuids The uuids of the queue items to remove. + * @return {number} The number of items removed from the queue. + */ +Player.prototype.removeQueueItems = function(uuids) { + let currentWindowRemoved = false; + let lowestIndexRemoved = this.queue_.length - 1; + const initialQueueSize = this.queue_.length; + // Sort in descending order to start removing from the end. + uuids = uuids.sort(this.uuidComparator_); + uuids.forEach((uuid) => { + const indexToRemove = this.queueUuidIndexMap_[uuid]; + if (indexToRemove === undefined) { + return; + } + // Remove the item from the queue. + this.queue_.splice(indexToRemove, 1); + // Remove the corresponding media item info. + delete this.mediaItemInfoMap_[uuid]; + // Remove the mapping to the window index. + delete this.queueUuidIndexMap_[uuid]; + lowestIndexRemoved = Math.min(lowestIndexRemoved, indexToRemove); + currentWindowRemoved = + currentWindowRemoved || indexToRemove === this.windowIndex_; + // The window index needs to be decreased when the item which has been + // removed was before the current item, when the current item at the last + // position has been removed, or when the queue has been emptied. + if (indexToRemove < this.windowIndex_ || + (indexToRemove === this.windowIndex_ && + indexToRemove === this.queue_.length) || + this.queue_.length === 0) { + this.windowIndex_--; + } + // Adjust the shuffle order. + let shuffleIndexToRemove; + this.shuffleOrder_.forEach((windowIndex, index) => { + if (windowIndex > indexToRemove) { + // Decrease the index in the shuffle order. + this.shuffleOrder_[index]--; + } else if (windowIndex === indexToRemove) { + // Recall index for removal after traversing. + shuffleIndexToRemove = index; + } + }); + // Remove the shuffle order entry of the removed item. + this.shuffleOrder_.splice(shuffleIndexToRemove, 1); + }); + const removedItemsCount = initialQueueSize - this.queue_.length; + if (removedItemsCount === 0) { + return 0; + } + this.updateShuffleIndex_(); + this.buildUuidIndexMap_(lowestIndexRemoved); + if (currentWindowRemoved) { + if (this.queue_.length === 0) { + this.playbackState_ = this.playbackState_ === PlaybackState.IDLE ? + PlaybackState.IDLE : + PlaybackState.ENDED; + this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; + this.windowPeriodIndex_ = 0; + this.videoElement_.currentTime = 0; + this.uuidToPrepare_ = null; + this.unregisterManifestResponseFilter_(); + this.unload_(/** reinitialiseMediaSource= */ true); + } else if (this.windowIndex_ >= 0) { + const windowIndexToPrepare = this.windowIndex_; + this.windowIndex_ = INDEX_UNSET; + this.seekToWindowInternal_(windowIndexToPrepare, undefined); + return removedItemsCount; + } + } + this.invalidate(); + return removedItemsCount; +}; + +/** + * Move the queue item with the given id to the given position. + * + * @param {string} uuid The uuid of the queue item to move. + * @param {number} to The position to move the item to. + * @param {!Array|undefined} shuffleOrder The new shuffle order. + * @return {boolean} Whether the item has been moved. + */ +Player.prototype.moveQueueItem = function(uuid, to, shuffleOrder) { + if (to < 0 || to >= this.queue_.length) { + return false; + } + const windowIndex = this.queueUuidIndexMap_[uuid]; + if (windowIndex === undefined) { + return false; + } + const itemMoved = this.moveInQueue_(windowIndex, to); + if (itemMoved) { + this.setShuffleOrder_(shuffleOrder); + this.invalidate(); + } + return itemMoved; +}; + +/** + * Prepares the player at the current window index and position. + * + * The playback state immediately transitions to `BUFFERING`. If the queue + * is empty the player transitions to `ENDED`. + */ +Player.prototype.prepare = function() { + if (this.queue_.length === 0) { + this.uuidToPrepare_ = null; + this.playbackState_ = PlaybackState.ENDED; + this.invalidate(); + return; + } + if (this.uuidToPrepare_) { + this.windowIndex_ = + this.queueUuidIndexMap_[this.uuidToPrepare_] || INDEX_UNSET; + this.uuidToPrepare_ = null; + } + this.windowIndex_ = clamp(this.windowIndex_, 0, this.queue_.length - 1); + this.prepare_(this.getCurrentPositionMs()); + this.invalidate(); +}; + +/** + * Stops the player. + * + * Calling this method causes the player to transition into `IDLE` state. + * If `reset` is `true` the player is reset to the initial state of right + * after construction. If `reset` is `false`, the media queue is preserved + * and calling `prepare()` results in resuming the player state to what it + * was before calling `#stop(false)`. + * + * @param {boolean} reset Whether the state should be reset. + * @return {!Promise} A promise which resolves after async unload + * tasks have finished. + */ +Player.prototype.stop = function(reset) { + this.playbackState_ = PlaybackState.IDLE; + this.playbackError_ = null; + this.discontinuityReason_ = null; + this.unregisterManifestResponseFilter_(); + this.uuidToPrepare_ = this.uuidToPrepare_ || (this.queue_[this.windowIndex_] ? + this.queue_[this.windowIndex_].uuid : + null); + if (reset) { + this.uuidToPrepare_ = null; + this.queue_ = []; + this.queueUuidIndexMap_ = {}; + this.uuidComparator_ = createUuidComparator(this.queueUuidIndexMap_); + this.windowIndex_ = INDEX_UNSET; + this.mediaItemInfoMap_ = {}; + this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; + this.windowPeriodIndex_ = 0; + this.videoElement_.currentTime = 0; + this.shuffleOrder_ = []; + this.shuffleIndex_ = 0; + } + this.invalidate(); + return this.unload_(/** reinitialiseMediaSource= */ !reset); +}; + +/** + * Resets player and media element. + * + * @private + * @param {boolean} reinitialiseMediaSource Whether the media source should be + * reinitialized. + * @return {!Promise} A promise which resolves after async unload + * tasks have finished. + */ +Player.prototype.unload_ = function(reinitialiseMediaSource) { + const playbackTypeToUnload = this.playbackType_; + this.playbackType_ = PlaybackType.UNKNOWN; + switch (playbackTypeToUnload) { + case PlaybackType.VIDEO_ELEMENT: + this.videoElement_.removeAttribute('src'); + this.videoElement_.load(); + return Promise.resolve(); + case PlaybackType.SHAKA_PLAYER: + return new Promise((resolve, reject) => { + this.shakaPlayer_.unload(reinitialiseMediaSource) + .then(resolve) + .catch(resolve); + }); + default: + return Promise.resolve(); + } +}; + +/** + * Releases the current Shaka instance and create a new one. + * + * This function should only be called if the Shaka instance is out of order due + * to https://github.com/google/shaka-player/issues/1785. It assumes the current + * Shaka instance has fallen into a state in which promises returned by + * `shakaPlayer.load` and `shakaPlayer.unload` do not resolve nor are they + * rejected anymore. + * + * @private + */ +Player.prototype.replaceShaka_ = function() { + // Remove all listeners. + this.shakaPlayer_.removeEventListener( + ShakaEvent.STREAMING, this.playbackStateListener_); + this.shakaPlayer_.removeEventListener( + ShakaEvent.TRACKS_CHANGED, this.playbackStateListener_); + this.shakaPlayer_.removeEventListener( + ShakaEvent.ERROR, this.shakaErrorHandler_); + // Unregister response filter if any. + this.unregisterManifestResponseFilter_(); + // Unload the old instance. + this.shakaPlayer_.unload(false); + // Reset video element. + this.videoElement_.removeAttribute('src'); + this.videoElement_.load(); + // Create a new instance and add listeners. + this.shakaPlayer_ = new ShakaPlayer(this.videoElement_); + this.shakaPlayer_.addEventListener( + ShakaEvent.STREAMING, this.playbackStateListener_); + this.shakaPlayer_.addEventListener( + ShakaEvent.TRACKS_CHANGED, this.playbackStateListener_); + this.shakaPlayer_.addEventListener(ShakaEvent.ERROR, this.shakaErrorHandler_); +}; + +/** + * Moves a queue item within the queue. + * + * @private + * @param {number} from The initial position. + * @param {number} to The position to move the item to. + * @return {boolean} Whether the item has been moved. + */ +Player.prototype.moveInQueue_ = function(from, to) { + if (from < 0 || to < 0 + || from >= this.queue_.length || to >= this.queue_.length + || from === to) { + return false; + } + this.queue_.splice(to, 0, this.queue_.splice(from, 1)[0]); + this.buildUuidIndexMap_(Math.min(from, to)); + if (from === this.windowIndex_) { + this.windowIndex_ = to; + } else if (from > this.windowIndex_ && to <= this.windowIndex_) { + this.windowIndex_++; + } else if (from < this.windowIndex_ && to >= this.windowIndex_) { + this.windowIndex_--; + } + return true; +}; + +/** + * Shuffles the queue. + * + * @private + */ +Player.prototype.shuffle_ = function() { + this.shuffleOrder_ = this.queue_.map((item, index) => index); + googArray.shuffle(this.shuffleOrder_); + this.updateShuffleIndex_(); +}; + +/** + * Sets the new shuffle order. + * + * @private + * @param {!Array|undefined} shuffleOrder The new shuffle order. + */ +Player.prototype.setShuffleOrder_ = function(shuffleOrder) { + if (shuffleOrder && this.queue_.length === shuffleOrder.length) { + this.shuffleOrder_ = shuffleOrder; + this.updateShuffleIndex_(); + } else if (this.shuffleOrder_.length !== this.queue_.length) { + this.shuffle_(); + } +}; + +/** + * Updates the shuffle order to point to the current window index. + * + * @private + */ +Player.prototype.updateShuffleIndex_ = function() { + this.shuffleIndex_ = + this.shuffleOrder_.findIndex((idx) => idx === this.windowIndex_); +}; + +/** + * Builds the `queueUuidIndexMap` using the uuid of a media item as the key and + * the window index as the value of an entry. + * + * @private + * @param {number} startPosition The window index to start updating at. + */ +Player.prototype.buildUuidIndexMap_ = function(startPosition) { + for (let i = startPosition; i < this.queue_.length; i++) { + this.queueUuidIndexMap_[this.queue_[i].uuid] = i; + } +}; + +/** + * Gets the default position of the current window. + * + * @private + * @return {number} The default position of the current window. + */ +Player.prototype.getDefaultPosition_ = function() { + return this.windowMediaItemInfo_.defaultStartPositionUs; +}; + +/** + * Checks whether the given position is buffered. + * + * @private + * @param {number} positionMs The position to check. + * @return {boolean} true if the media data of the current position is buffered. + */ +Player.prototype.isBuffered_ = function(positionMs) { + const ranges = this.videoElement_.buffered; + for (let i = 0; i < ranges.length; i++) { + const start = ranges.start(i) * 1000; + const end = ranges.end(i) * 1000; + if (start <= positionMs && positionMs <= end) { + return true; + } + } + return false; +}; + +/** + * Seeks to the positionMs of the given window. + * + * To signal a user seek, callers are expected to set the discontinuity reason + * to `DiscontinuityReason.SEEK` before calling this method. If not set this + * method may set the `DiscontinuityReason.PERIOD_TRANSITION` in case the + * `windowIndex` changes. + * + * @private + * @param {number} windowIndex The non-negative index of the window to seek to. + * @param {number|undefined} positionMs The position to seek to within the + * window. If undefined it seeks to the default position of the window. + */ +Player.prototype.seekToWindowInternal_ = function(windowIndex, positionMs) { + const windowChanges = this.windowIndex_ !== windowIndex; + // Update window index and position in any case. + this.windowIndex_ = Math.max(0, windowIndex); + this.updateShuffleIndex_(); + const seekPositionMs = this.getPosition_(positionMs, windowIndex); + this.videoElement_.currentTime = seekPositionMs / 1000; + + // IDLE or ENDED with empty queue. + if (this.playbackState_ === PlaybackState.IDLE || this.queue_.length === 0) { + // Do nothing but report the change in window index and position. + this.invalidate(); + return; + } + + // Prepare for a seek to another window or when in ENDED state whilst the + // queue is not empty but prepare has not been called yet. + if (windowChanges || this.playbackType_ === PlaybackType.UNKNOWN) { + // Reset and prepare. + this.unregisterManifestResponseFilter_(); + this.discontinuityReason_ = + this.discontinuityReason_ || DiscontinuityReason.PERIOD_TRANSITION; + this.prepare_(seekPositionMs); + this.invalidate(); + return; + } + + // Sync playWhenReady with video element after ENDED state. + if (this.playbackState_ === PlaybackState.ENDED && this.playWhenReady_) { + this.videoElement_.play(); + return; + } + + // A seek within the current window when READY or BUFFERING. + this.playbackState_ = this.isBuffered_(seekPositionMs) ? + PlaybackState.READY : + PlaybackState.BUFFERING; + this.invalidate(); +}; + +/** + * Prepares the player at the current window index and the given + * `startPositionMs`. + * + * Calling this method resets the media item information, transitions to + * 'BUFFERING', prepares either the plain video element for progressive + * media, or the Shaka player for adaptive media. + * + * Media items are mapped by media type to a `PlaybackType`s in + * `exoplayer.cast.constants.SupportedMediaTypes`. Unsupported mime types will + * cause the player to transition to the `IDLE` state. + * + * Items in the queue are expected to have been validated with + * `validation#validateMediaItem` or `validation#validateMediaItems`. If this is + * not the case this method might throw an Assertion exception. + * + * @private + * @param {number} startPositionMs The position at which to start playback. + * @throws {!AssertionException} In case an unvalidated item can't be mapped to + * a supported playback type. + */ +Player.prototype.prepare_ = function(startPositionMs) { + const mediaItem = this.queue_[this.windowIndex_]; + const windowUuid = this.queue_[this.windowIndex_].uuid; + const mediaItemInfo = this.mediaItemInfoMap_[windowUuid]; + if (mediaItemInfo && !mediaItemInfo.isDynamic) { + // Do reuse if not dynamic. + this.windowMediaItemInfo_ = mediaItemInfo; + } else { + // Use the dummy info until manifest/data available. + this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; + this.mediaItemInfoMap_[windowUuid] = DUMMY_MEDIA_ITEM_INFO; + } + this.windowPeriodIndex_ = 0; + this.playbackType_ = getPlaybackType(mediaItem.mimeType); + this.playbackState_ = PlaybackState.BUFFERING; + const uri = mediaItem.media.uri; + switch (this.playbackType_) { + case PlaybackType.VIDEO_ELEMENT: + this.videoElement_.currentTime = startPositionMs / 1000; + this.shakaPlayer_.unload(false) + .then(() => { + this.setMediaElementSrc(uri); + this.videoElement_.currentTime = startPositionMs / 1000; + }) + .catch((error) => { + // Let's still try. We actually don't need Shaka right now. + this.setMediaElementSrc(uri); + this.videoElement_.currentTime = startPositionMs / 1000; + console.error('Shaka error while unloading', error); + }); + break; + case PlaybackType.SHAKA_PLAYER: + this.shakaPlayer_.configure( + this.configurationFactory_.createConfiguration( + mediaItem, this.trackSelectionParameters_)); + this.shakaPlayer_.load(uri, startPositionMs / 1000).catch((error) => { + const shakaError = /** @type {!ShakaError} */ (error); + if (shakaError.severity !== ShakaError.Severity.RECOVERABLE && + shakaError.code !== ShakaError.Code.LOAD_INTERRUPTED) { + this.fatalShakaError_(shakaError, 'loading failed for uri: ' + uri); + this.invalidate(); + } else { + console.error('Recoverable Shaka error while loading', shakaError); + } + }); + break; + default: + fail('unknown playback type for mime type: ' + mediaItem.mimeType); + } +}; + +/** + * Sets the uri to the `src` attribute of the media element in a safe way. + * + * @param {string} uri The uri to set as the value of the `src` attribute. + */ +Player.prototype.setMediaElementSrc = function(uri) { + safedom.setVideoSrc( + asserts.assertIsHTMLVideoElement(this.videoElement_), uri); +}; + +/** + * Handles a fatal Shaka error by setting the playback error, transitioning to + * state `IDLE` and setting the playback type to `UNKNOWN`. Player needs to be + * reprepared after calling this method. + * + * @private + * @param {!ShakaError} shakaError The error. + * @param {string|undefined} customMessage A custom message. + */ +Player.prototype.fatalShakaError_ = function(shakaError, customMessage) { + this.playbackState_ = PlaybackState.IDLE; + this.playbackType_ = PlaybackType.UNKNOWN; + this.uuidToPrepare_ = this.queue_[this.windowIndex_] ? + this.queue_[this.windowIndex_].uuid : + null; + if (typeof shakaError.severity === 'undefined') { + // Not a Shaka error. We need to assume the worst case. + this.replaceShaka_(); + this.playbackError_ = /** @type {!PlayerError} */ ({ + message: ErrorMessages.UNKNOWN_FATAL_ERROR, + code: -1, + category: ErrorCategory.FATAL_SHAKA_ERROR, + }); + } else { + // A critical ShakaError. Can be recovered from by calling prepare. + this.playbackError_ = /** @type {!PlayerError} */ ({ + message: customMessage || shakaError.message || + ErrorMessages.SHAKA_UNKNOWN_ERROR, + code: shakaError.code, + category: shakaError.category, + }); + } + console.error('caught shaka load error', shakaError); +}; + +/** + * Gets the position to use. If `undefined` or `null` is passed as argument the + * default start position of the media item info of the given windowIndex is + * returned. + * + * @private + * @param {?number|undefined} positionMs The position in milliseconds, + * `undefined` or `null`. + * @param {number} windowIndex The window index for which to evaluate the + * position. + * @return {number} The position to use in milliseconds. + */ +Player.prototype.getPosition_ = function(positionMs, windowIndex) { + if (positionMs !== undefined) { + return Math.max(0, positionMs); + } + const windowUuid = assert(this.queue_[windowIndex]).uuid; + const mediaItemInfo = + this.mediaItemInfoMap_[windowUuid] || DUMMY_MEDIA_ITEM_INFO; + return mediaItemInfo.defaultStartPositionUs; +}; + +/** + * Refreshes the media item info of the current window. + * + * @private + */ +Player.prototype.updateWindowMediaItemInfo_ = function() { + this.windowMediaItemInfo_ = this.buildMediaItemInfo_(); + if (this.windowMediaItemInfo_) { + const mediaItem = this.queue_[this.windowIndex_]; + this.mediaItemInfoMap_[mediaItem.uuid] = this.windowMediaItemInfo_; + this.evaluateAndSetCurrentPeriod_(this.windowMediaItemInfo_.periods); + } +}; + +/** + * Evaluates the current period and stores it in a member variable. + * + * @private + * @param {!Array} periods The periods of the current mediaItem. + */ +Player.prototype.evaluateAndSetCurrentPeriod_ = function(periods) { + const positionUs = this.getCurrentPositionMs() * 1000; + let positionInWindowUs = 0; + periods.some((period, i) => { + positionInWindowUs += period.durationUs; + if (positionUs < positionInWindowUs) { + this.windowPeriodIndex_ = i; + return true; + } + return false; + }); +}; + +/** + * Registers a response filter which is notified when a manifest has been + * downloaded. + * + * @private + */ +Player.prototype.registerManifestResponseFilter_ = function() { + if (this.isManifestFilterRegistered_) { + return; + } + this.shakaPlayer_.getNetworkingEngine().registerResponseFilter( + this.manifestResponseFilter_); + this.isManifestFilterRegistered_ = true; +}; + +/** + * Unregisters the manifest response filter. + * + * @private + */ +Player.prototype.unregisterManifestResponseFilter_ = function() { + if (this.isManifestFilterRegistered_) { + this.shakaPlayer_.getNetworkingEngine().unregisterResponseFilter( + this.manifestResponseFilter_); + this.isManifestFilterRegistered_ = false; + } +}; + +/** + * Builds a MediaItemInfo from the media element. + * + * @private + * @return {!MediaItemInfo} A media item info. + */ +Player.prototype.buildMediaItemInfoFromElement_ = function() { + const durationUs = this.videoElement_.duration * 1000 * 1000; + return /** @type {!MediaItemInfo} */ ({ + isSeekable: !!this.videoElement_.seekable, + isDynamic: false, + positionInFirstPeriodUs: 0, + defaultStartPositionUs: 0, + windowDurationUs: durationUs, + periods: [{ + id: 0, + durationUs: durationUs, + }], + }); +}; + +/** + * Builds a MediaItemInfo from the manifest or null if no manifest is available. + * + * @private + * @return {!MediaItemInfo} + */ +Player.prototype.buildMediaItemInfo_ = function() { + const manifest = this.shakaPlayer_.getManifest(); + if (manifest === null) { + return DUMMY_MEDIA_ITEM_INFO; + } + const timeline = manifest.presentationTimeline; + const isDynamic = timeline.isLive(); + const windowStartUs = isDynamic ? + timeline.getSeekRangeStart() * 1000 * 1000 : + timeline.getSegmentAvailabilityStart() * 1000 * 1000; + const windowDurationUs = isDynamic ? + (timeline.getSeekRangeEnd() - timeline.getSeekRangeStart()) * 1000 * + 1000 : + timeline.getDuration() * 1000 * 1000; + const defaultStartPositionUs = isDynamic ? + timeline.getSeekRangeEnd() * 1000 * 1000 : + timeline.getSegmentAvailabilityStart() * 1000 * 1000; + + const periods = []; + let previousStartTimeUs = 0; + let positionInFirstPeriodUs = 0; + manifest.periods.forEach((period, index) => { + const startTimeUs = period.startTime * 1000 * 1000; + periods.push({ + id: Math.floor(startTimeUs), + }); + if (index > 0) { + // calculate duration of previous period + periods[index - 1].durationUs = startTimeUs - previousStartTimeUs; + if (previousStartTimeUs <= windowStartUs && windowStartUs < startTimeUs) { + positionInFirstPeriodUs = windowStartUs - previousStartTimeUs; + } + } + previousStartTimeUs = startTimeUs; + }); + // calculate duration of last period + if (periods.length) { + const lastPeriodDurationUs = + isDynamic ? Infinity : windowDurationUs - previousStartTimeUs; + periods.slice(-1)[0].durationUs = lastPeriodDurationUs; + if (previousStartTimeUs <= windowStartUs) { + positionInFirstPeriodUs = windowStartUs - previousStartTimeUs; + } + } + return /** @type {!MediaItemInfo} */ ({ + windowDurationUs: Math.floor(windowDurationUs), + defaultStartPositionUs: Math.floor(defaultStartPositionUs), + isSeekable: this.videoElement_ ? !!this.videoElement_.seekable : false, + positionInFirstPeriodUs: Math.floor(positionInFirstPeriodUs), + isDynamic: isDynamic, + periods: periods, + }); +}; + +/** + * Builds the player state message. + * + * @private + * @return {!PlayerState} The player state. + */ +Player.prototype.buildPlayerState_ = function() { + const playerState = { + playbackState: this.getPlaybackState(), + playbackParameters: { + speed: 1, + pitch: 1, + skipSilence: false, + }, + playbackPosition: this.buildPlaybackPosition_(), + playWhenReady: this.getPlayWhenReady(), + windowIndex: this.getCurrentWindowIndex(), + windowCount: this.queue_.length, + audioTracks: this.getAudioTracks() || [], + videoTracks: this.getVideoTracks(), + repeatMode: this.repeatMode_, + shuffleModeEnabled: this.shuffleModeEnabled_, + mediaQueue: this.queue_.slice(), + mediaItemsInfo: this.mediaItemInfoMap_, + shuffleOrder: this.shuffleOrder_, + sequenceNumber: -1, + }; + if (this.playbackError_) { + playerState.error = this.playbackError_; + this.playbackError_ = null; + } + return playerState; +}; + +/** + * Builds the playback position. Returns null if all properties of the playback + * position are empty. + * + * @private + * @return {?PlaybackPosition} The playback position. + */ +Player.prototype.buildPlaybackPosition_ = function() { + if ((this.playbackState_ === PlaybackState.IDLE && !this.uuidToPrepare_) || + this.playbackState_ === PlaybackState.ENDED && this.queue_.length === 0) { + this.discontinuityReason_ = null; + return null; + } + /** @type {!PlaybackPosition} */ + const playbackPosition = { + positionMs: this.getCurrentPositionMs(), + uuid: this.uuidToPrepare_ || this.queue_[this.windowIndex_].uuid, + periodId: this.windowMediaItemInfo_.periods[this.windowPeriodIndex_].id, + discontinuityReason: null, + }; + if (this.discontinuityReason_ !== null) { + playbackPosition.discontinuityReason = this.discontinuityReason_; + this.discontinuityReason_ = null; + } + return playbackPosition; +}; + +exports = Player; +exports.RepeatMode = RepeatMode; +exports.PlaybackState = PlaybackState; +exports.DiscontinuityReason = DiscontinuityReason; +exports.DUMMY_MEDIA_ITEM_INFO = DUMMY_MEDIA_ITEM_INFO; diff --git a/cast_receiver_app/src/timeout.js b/cast_receiver_app/src/timeout.js new file mode 100644 index 0000000000..e5df5ec2f4 --- /dev/null +++ b/cast_receiver_app/src/timeout.js @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2019 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. + */ + +goog.module('exoplayer.cast.Timeout'); + +/** + * A timeout which can be cancelled. + */ +class Timeout { + constructor() { + /** @private {?number} */ + this.timeout_ = null; + } + /** + * Returns a promise which resolves when the duration of time defined by + * delayMs has elapsed and cancel() has not been called earlier. + * + * If the timeout is already set, the former timeout is cancelled and a new + * one is started. + * + * @param {number} delayMs The delay after which to resolve or a non-positive + * value if it should never resolve. + * @return {!Promise} Resolves after the given delayMs or never + * for a non-positive delay. + */ + postDelayed(delayMs) { + this.cancel(); + return new Promise((resolve, reject) => { + if (delayMs <= 0) { + return; + } + this.timeout_ = setTimeout(() => { + if (this.timeout_) { + this.timeout_ = null; + resolve(); + } + }, delayMs); + }); + } + + /** Cancels the timeout. */ + cancel() { + if (this.timeout_) { + clearTimeout(this.timeout_); + this.timeout_ = null; + } + } + + /** @return {boolean} true if the timeout is currently ongoing. */ + isOngoing() { + return this.timeout_ !== null; + } +} + +exports = Timeout; diff --git a/cast_receiver_app/src/util.js b/cast_receiver_app/src/util.js new file mode 100644 index 0000000000..75afd9e5d3 --- /dev/null +++ b/cast_receiver_app/src/util.js @@ -0,0 +1,62 @@ +/* + * 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. + */ + +goog.module('exoplayer.cast.util'); + +/** + * Indicates whether the logging is turned on. + */ +const enableLogging = true; + +/** + * Logs to the console if logging enabled. + * + * @param {!Array<*>} statements The log statements to be logged. + */ +const log = function(statements) { + if (enableLogging) { + console.log.apply(console, statements); + } +}; + +/** + * A comparator function for uuids. + * + * @typedef {function(string,string):number} + */ +let UuidComparator; + +/** + * Creates a comparator function which sorts uuids in descending order by the + * corresponding index of the given map. + * + * @param {!Object} uuidIndexMap The map with uuids as the key + * and the window index as the value. + * @return {!UuidComparator} The comparator for sorting. + */ +const createUuidComparator = function(uuidIndexMap) { + return (a, b) => { + const indexA = uuidIndexMap[a] || -1; + const indexB = uuidIndexMap[b] || -1; + return indexB - indexA; + }; +}; + +exports = { + log, + createUuidComparator, + UuidComparator, +}; diff --git a/cast_receiver_app/test/caf_bootstrap.js b/cast_receiver_app/test/caf_bootstrap.js new file mode 100644 index 0000000000..721360e8a7 --- /dev/null +++ b/cast_receiver_app/test/caf_bootstrap.js @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2019 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. + */ + +/** + * @fileoverview Declares constants which are provided by the CAF externs and + * are not included in uncompiled unit tests. + */ +cast = { + framework: { + system: { + EventType: { + SENDER_CONNECTED: 'sender_connected', + SENDER_DISCONNECTED: 'sender_disconnected', + }, + DisconnectReason: { + REQUESTED_BY_SENDER: 'requested_by_sender', + }, + }, + }, +}; diff --git a/cast_receiver_app/test/configuration_factory_test.js b/cast_receiver_app/test/configuration_factory_test.js new file mode 100644 index 0000000000..af9254c59e --- /dev/null +++ b/cast_receiver_app/test/configuration_factory_test.js @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2019 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. + */ + +goog.module('exoplayer.cast.test.configurationfactory'); +goog.setTestOnly(); + +const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); +const testSuite = goog.require('goog.testing.testSuite'); +const util = goog.require('exoplayer.cast.test.util'); + +let configurationFactory; + +testSuite({ + setUp() { + configurationFactory = new ConfigurationFactory(); + }, + + /** Tests creating the most basic configuration. */ + testCreateBasicConfiguration() { + /** @type {!TrackSelectionParameters} */ + const selectionParameters = /** @type {!TrackSelectionParameters} */ ({ + preferredAudioLanguage: 'en', + preferredTextLanguage: 'it', + }); + const configuration = configurationFactory.createConfiguration( + util.queue.slice(0, 1), selectionParameters); + assertEquals('en', configuration.preferredAudioLanguage); + assertEquals('it', configuration.preferredTextLanguage); + // Assert empty drm configuration as default. + assertArrayEquals(['servers'], Object.keys(configuration.drm)); + assertArrayEquals([], Object.keys(configuration.drm.servers)); + }, + + /** Tests defaults for undefined audio and text languages. */ + testCreateBasicConfiguration_languagesUndefined() { + const configuration = configurationFactory.createConfiguration( + util.queue.slice(0, 1), /** @type {!TrackSelectionParameters} */ ({})); + assertEquals('', configuration.preferredAudioLanguage); + assertEquals('', configuration.preferredTextLanguage); + }, + + /** Tests creating a drm configuration */ + testCreateDrmConfiguration() { + /** @type {!MediaItem} */ + const mediaItem = util.queue[1]; + mediaItem.drmSchemes = [ + { + uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', + licenseServer: { + uri: 'drm-uri0', + }, + }, + { + uuid: '9a04f079-9840-4286-ab92-e65be0885f95', + licenseServer: { + uri: 'drm-uri1', + }, + }, + { + uuid: 'unsupported-drm-uuid', + licenseServer: { + uri: 'drm-uri2', + }, + }, + ]; + const configuration = + configurationFactory.createConfiguration(mediaItem, {}); + assertEquals('drm-uri0', configuration.drm.servers['com.widevine.alpha']); + assertEquals( + 'drm-uri1', configuration.drm.servers['com.microsoft.playready']); + assertEquals(2, Object.entries(configuration.drm.servers).length); + } +}); diff --git a/cast_receiver_app/test/externs.js b/cast_receiver_app/test/externs.js new file mode 100644 index 0000000000..a90a367691 --- /dev/null +++ b/cast_receiver_app/test/externs.js @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2019 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. + */ + +/** + * Externs for unit tests to avoid renaming of properties. + * + * These externs are only required when building with bazel because the + * closure_js_test compiles tests as well. + * + * @externs + */ + +/** @record */ +function ValidationObject() {} + +/** @type {*} */ +ValidationObject.prototype.field; + +/** @record */ +function Uuids() {} + +/** @type {!Array} */ +Uuids.prototype.uuids; diff --git a/cast_receiver_app/test/message_dispatcher_test.js b/cast_receiver_app/test/message_dispatcher_test.js new file mode 100644 index 0000000000..3e7daaf573 --- /dev/null +++ b/cast_receiver_app/test/message_dispatcher_test.js @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2019 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. + * + * @fileoverview Unit tests for the message dispatcher. + */ + +goog.module('exoplayer.cast.test.messagedispatcher'); +goog.setTestOnly(); + +const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher'); +const mocks = goog.require('exoplayer.cast.test.mocks'); +const testSuite = goog.require('goog.testing.testSuite'); + +let contextMock; +let messageDispatcher; + +testSuite({ + setUp() { + mocks.setUp(); + contextMock = mocks.createCastReceiverContextFake(); + messageDispatcher = new MessageDispatcher( + 'urn:x-cast:com.google.exoplayer.cast', contextMock); + }, + + /** Test marshalling Infinity */ + testStringifyInfinity() { + const senderId = 'sender0'; + const name = 'Federico Vespucci'; + messageDispatcher.send(senderId, {name: name, duration: Infinity}); + + const msg = mocks.state().outputMessages[senderId][0]; + assertUndefined(msg.duration); + assertFalse(msg.hasOwnProperty('duration')); + assertEquals(name, msg.name); + assertTrue(msg.hasOwnProperty('name')); + } +}); diff --git a/cast_receiver_app/test/mocks.js b/cast_receiver_app/test/mocks.js new file mode 100644 index 0000000000..244ac72829 --- /dev/null +++ b/cast_receiver_app/test/mocks.js @@ -0,0 +1,277 @@ +/** + * 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. + * + * @fileoverview Mocks for testing cast components. + */ + +goog.module('exoplayer.cast.test.mocks'); +goog.setTestOnly(); + +const NetworkingEngine = goog.require('shaka.net.NetworkingEngine'); + +let mockState; +let manifest; + +/** + * Initializes the state of the mocks. Needs to be called in the setUp method of + * the unit test. + */ +const setUp = function() { + mockState = { + outputMessages: {}, + listeners: {}, + loadedUri: null, + preferredTextLanguage: '', + preferredAudioLanguage: '', + configuration: null, + responseFilter: null, + isSilent: false, + customMessageListener: undefined, + mediaElementState: { + removedAttributes: [], + }, + manifestState: { + isLive: false, + windowDuration: 20, + startTime: 0, + delay: 10, + }, + getManifest: () => manifest, + setManifest: (m) => { + manifest = m; + }, + shakaError: { + severity: /** CRITICAL */ 2, + code: /** not 7000 (LOAD_INTERUPTED) */ 3, + category: /** any */ 1, + }, + simulateLoad: simulateLoadSuccess, + /** @type {function(boolean)} */ + setShakaThrowsOnLoad: (doThrow) => { + mockState.simulateLoad = doThrow ? throwShakaError : simulateLoadSuccess; + }, + simulateUnload: simulateUnloadSuccess, + /** @type {function(boolean)} */ + setShakaThrowsOnUnload: (doThrow) => { + mockState.simulateUnload = + doThrow ? throwShakaError : simulateUnloadSuccess; + }, + onSenderConnected: undefined, + onSenderDisconnected: undefined, + }; + manifest = { + periods: [{startTime: mockState.manifestState.startTime}], + presentationTimeline: { + getDuration: () => mockState.manifestState.windowDuration, + isLive: () => mockState.manifestState.isLive, + getSegmentAvailabilityStart: () => 0, + getSegmentAvailabilityEnd: () => mockState.manifestState.windowDuration, + getSeekRangeStart: () => 0, + getSeekRangeEnd: () => mockState.manifestState.windowDuration - + mockState.manifestState.delay, + }, + }; +}; + +/** + * Simulates a successful `shakaPlayer.load` call. + * + * @param {string} uri The uri to load. + */ +const simulateLoadSuccess = (uri) => { + mockState.loadedUri = uri; + notifyListeners('streaming'); +}; + +/** Simulates a successful `shakaPlayer.unload` call. */ +const simulateUnloadSuccess = () => { + mockState.loadedUri = undefined; + notifyListeners('unloading'); +}; + +/** @throws {!ShakaError} Thrown in any case. */ +const throwShakaError = () => { + throw mockState.shakaError; +}; + + +/** + * Adds a fake event listener. + * + * @param {string} type The type of the listener. + * @param {function(!Object)} listener The callback listener. + */ +const addEventListener = function(type, listener) { + mockState.listeners[type] = mockState.listeners[type] || []; + mockState.listeners[type].push(listener); +}; + +/** + * Notifies the fake listeners of the given type. + * + * @param {string} type The type of the listener to notify. + */ +const notifyListeners = function(type) { + if (mockState.isSilent || !mockState.listeners[type]) { + return; + } + for (let i = 0; i < mockState.listeners[type].length; i++) { + mockState.listeners[type][i]({ + type: type + }); + } +}; + +/** + * Creates an observable for which listeners can be added. + * + * @return {!Object} An observable object. + */ +const createObservable = () => { + return { + addEventListener: (type, listener) => { + addEventListener(type, listener); + }, + }; +}; + +/** + * Creates a fake for the shaka player. + * + * @return {!shaka.Player} A shaka player mock object. + */ +const createShakaFake = () => { + const shakaFake = /** @type {!shaka.Player} */(createObservable()); + const mediaElement = createMediaElementFake(); + /** + * @return {!HTMLMediaElement} A media element. + */ + shakaFake.getMediaElement = () => mediaElement; + shakaFake.getAudioLanguages = () => []; + shakaFake.getVariantTracks = () => []; + shakaFake.configure = (configuration) => { + mockState.configuration = configuration; + return true; + }; + shakaFake.selectTextLanguage = (language) => { + mockState.preferredTextLanguage = language; + }; + shakaFake.selectAudioLanguage = (language) => { + mockState.preferredAudioLanguage = language; + }; + shakaFake.getManifest = () => manifest; + shakaFake.unload = async () => mockState.simulateUnload(); + shakaFake.load = async (uri) => mockState.simulateLoad(uri); + shakaFake.getNetworkingEngine = () => { + return /** @type {!NetworkingEngine} */ ({ + registerResponseFilter: (responseFilter) => { + mockState.responseFilter = responseFilter; + }, + unregisterResponseFilter: (responseFilter) => { + if (mockState.responseFilter !== responseFilter) { + throw new Error('unregistering invalid response filter'); + } else { + mockState.responseFilter = null; + } + }, + }); + }; + return shakaFake; +}; + +/** + * Creates a fake for a media element. + * + * @return {!HTMLMediaElement} A media element fake. + */ +const createMediaElementFake = () => { + const mediaElementFake = /** @type {!HTMLMediaElement} */(createObservable()); + mediaElementFake.load = () => { + // Do nothing. + }; + mediaElementFake.play = () => { + mediaElementFake.paused = false; + notifyListeners('playing'); + return Promise.resolve(); + }; + mediaElementFake.pause = () => { + mediaElementFake.paused = true; + notifyListeners('pause'); + }; + mediaElementFake.seekable = /** @type {!TimeRanges} */({ + length: 1, + start: (index) => mockState.manifestState.startTime, + end: (index) => mockState.manifestState.windowDuration, + }); + mediaElementFake.removeAttribute = (name) => { + mockState.mediaElementState.removedAttributes.push(name); + if (name === 'src') { + mockState.loadedUri = null; + } + }; + mediaElementFake.hasAttribute = (name) => { + return name === 'src' && !!mockState.loadedUri; + }; + mediaElementFake.buffered = /** @type {!TimeRanges} */ ({ + length: 0, + start: (index) => null, + end: (index) => null, + }); + mediaElementFake.paused = true; + return mediaElementFake; +}; + +/** + * Creates a cast receiver manager fake. + * + * @return {!Object} A cast receiver manager fake. + */ +const createCastReceiverContextFake = () => { + return { + addCustomMessageListener: (namespace, listener) => { + mockState.customMessageListener = listener; + }, + sendCustomMessage: (namespace, senderId, message) => { + mockState.outputMessages[senderId] = + mockState.outputMessages[senderId] || []; + mockState.outputMessages[senderId].push(message); + }, + addEventListener: (eventName, listener) => { + switch (eventName) { + case 'sender_connected': + mockState.onSenderConnected = listener; + break; + case 'sender_disconnected': + mockState.onSenderDisconnected = listener; + break; + } + }, + getSenders: () => [{id: 'sender0'}], + start: () => {}, + }; +}; + +/** + * Returns the state of the mocks. + * + * @return {?Object} + */ +const state = () => mockState; + +exports.createCastReceiverContextFake = createCastReceiverContextFake; +exports.createShakaFake = createShakaFake; +exports.notifyListeners = notifyListeners; +exports.setUp = setUp; +exports.state = state; diff --git a/cast_receiver_app/test/playback_info_view_test.js b/cast_receiver_app/test/playback_info_view_test.js new file mode 100644 index 0000000000..87cefe1884 --- /dev/null +++ b/cast_receiver_app/test/playback_info_view_test.js @@ -0,0 +1,242 @@ +/** + * Copyright (C) 2019 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. + * + * @fileoverview Unit tests for the playback info view. + */ + +goog.module('exoplayer.cast.test.PlaybackInfoView'); +goog.setTestOnly(); + +const PlaybackInfoView = goog.require('exoplayer.cast.PlaybackInfoView'); +const Player = goog.require('exoplayer.cast.Player'); +const testSuite = goog.require('goog.testing.testSuite'); + +/** The state of the player mock */ +let mockState; + +/** + * Initializes the state of the mock. Needs to be called in the setUp method of + * the unit test. + */ +const setUpMockState = function() { + mockState = { + playWhenReady: false, + currentPositionMs: 1000, + durationMs: 10 * 1000, + playbackState: 'READY', + discontinuityReason: undefined, + listeners: [], + currentMediaItem: { + mimeType: 'video/*', + }, + }; +}; + +/** Notifies registered listeners with the current player state. */ +const notifyListeners = function() { + if (!mockState) { + console.warn( + 'mock state not initialized. Did you call setUp ' + + 'when setting up the test case?'); + } + mockState.listeners.forEach((listener) => { + listener({ + playWhenReady: mockState.playWhenReady, + playbackState: mockState.playbackState, + playbackPosition: { + currentPositionMs: mockState.currentPositionMs, + discontinuityReason: mockState.discontinuityReason, + }, + }); + }); +}; + +/** + * Creates a sufficient mock of the Player. + * + * @return {!Player} + */ +const createPlayerMock = function() { + return /** @type {!Player} */ ({ + addPlayerListener: (listener) => { + mockState.listeners.push(listener); + }, + getPlayWhenReady: () => mockState.playWhenReady, + getPlaybackState: () => mockState.playbackState, + getCurrentPositionMs: () => mockState.currentPositionMs, + getDurationMs: () => mockState.durationMs, + getCurrentMediaItem: () => mockState.currentMediaItem, + }); +}; + +/** Inserts the DOM structure the playback info view needs. */ +const insertComponentDom = function() { + const container = appendChild(document.body, 'div', 'container-id'); + appendChild(container, 'div', 'exo_elapsed_time'); + appendChild(container, 'div', 'exo_elapsed_time_label'); + appendChild(container, 'div', 'exo_duration_label'); +}; + +/** + * Creates and appends a child to the parent element. + * + * @param {!Element} parent The parent element. + * @param {string} tagName The tag name of the child element. + * @param {string} id The id of the child element. + * @return {!Element} The appended child element. + */ +const appendChild = function(parent, tagName, id) { + const child = document.createElement(tagName); + child.id = id; + parent.appendChild(child); + return child; +}; + +/** Removes the inserted elements from the DOM again. */ +const removeComponentDom = function() { + const container = document.getElementById('container-id'); + if (container) { + container.parentNode.removeChild(container); + } +}; + +let playbackInfoView; + +testSuite({ + setUp() { + insertComponentDom(); + setUpMockState(); + playbackInfoView = new PlaybackInfoView( + createPlayerMock(), /** containerId= */ 'container-id'); + playbackInfoView.setShowTimeoutMs(1); + }, + + tearDown() { + removeComponentDom(); + }, + + /** Tests setting the show timeout. */ + testSetShowTimeout() { + assertEquals(1, playbackInfoView.showTimeoutMs_); + playbackInfoView.setShowTimeoutMs(10); + assertEquals(10, playbackInfoView.showTimeoutMs_); + }, + + /** Tests rendering the duration to the DOM. */ + testRenderDuration() { + const el = document.getElementById('exo_duration_label'); + assertEquals('00:10', el.firstChild.firstChild.nodeValue); + mockState.durationMs = 35 * 1000; + notifyListeners(); + assertEquals('00:35', el.firstChild.firstChild.nodeValue); + + mockState.durationMs = + (12 * 60 * 60 * 1000) + (20 * 60 * 1000) + (13 * 1000); + notifyListeners(); + assertEquals('12:20:13', el.firstChild.firstChild.nodeValue); + + mockState.durationMs = -1000; + notifyListeners(); + assertNull(el.nodeValue); + }, + + /** Tests rendering the playback position to the DOM. */ + testRenderPlaybackPosition() { + const el = document.getElementById('exo_elapsed_time_label'); + assertEquals('00:01', el.firstChild.firstChild.nodeValue); + mockState.currentPositionMs = 2000; + notifyListeners(); + assertEquals('00:02', el.firstChild.firstChild.nodeValue); + + mockState.currentPositionMs = + (12 * 60 * 60 * 1000) + (20 * 60 * 1000) + (13 * 1000); + notifyListeners(); + assertEquals('12:20:13', el.firstChild.firstChild.nodeValue); + + mockState.currentPositionMs = -1000; + notifyListeners(); + assertNull(el.nodeValue); + + mockState.currentPositionMs = 0; + notifyListeners(); + assertEquals('00:00', el.firstChild.firstChild.nodeValue); + }, + + /** Tests rendering the timebar width reflects position and duration. */ + testRenderTimebar() { + const el = document.getElementById('exo_elapsed_time'); + assertEquals('10%', el.style.width); + + mockState.currentPositionMs = 0; + notifyListeners(); + assertEquals('0px', el.style.width); + + mockState.currentPositionMs = 5 * 1000; + notifyListeners(); + assertEquals('50%', el.style.width); + + mockState.currentPositionMs = mockState.durationMs * 2; + notifyListeners(); + assertEquals('100%', el.style.width); + + mockState.currentPositionMs = -1; + notifyListeners(); + assertEquals('0px', el.style.width); + }, + + /** Tests whether the update timeout is set and removed. */ + testUpdateTimeout_setAndRemoved() { + assertFalse(playbackInfoView.updateTimeout_.isOngoing()); + + mockState.playWhenReady = true; + notifyListeners(); + assertTrue(playbackInfoView.updateTimeout_.isOngoing()); + + mockState.playWhenReady = false; + notifyListeners(); + assertFalse(playbackInfoView.updateTimeout_.isOngoing()); + }, + + /** Tests whether the show timeout is set when playback starts. */ + testHideTimeout_setAndRemoved() { + assertFalse(playbackInfoView.hideTimeout_.isOngoing()); + + mockState.playWhenReady = true; + notifyListeners(); + assertNotUndefined(playbackInfoView.hideTimeout_); + assertTrue(playbackInfoView.hideTimeout_.isOngoing()); + + mockState.playWhenReady = false; + notifyListeners(); + assertFalse(playbackInfoView.hideTimeout_.isOngoing()); + }, + + /** Test whether the view switches to always on for audio media. */ + testAlwaysOnForAudio() { + playbackInfoView.setShowTimeoutMs(50); + assertEquals(50, playbackInfoView.showTimeoutMs_); + // The player transitions from video to audio stream. + mockState.discontinuityReason = 'PERIOD_TRANSITION'; + mockState.currentMediaItem.mimeType = 'audio/*'; + notifyListeners(); + assertEquals(0, playbackInfoView.showTimeoutMs_); + + mockState.discontinuityReason = 'PERIOD_TRANSITION'; + mockState.currentMediaItem.mimeType = 'video/*'; + notifyListeners(); + assertEquals(50, playbackInfoView.showTimeoutMs_); + }, + +}); diff --git a/cast_receiver_app/test/player_test.js b/cast_receiver_app/test/player_test.js new file mode 100644 index 0000000000..96dfbf8614 --- /dev/null +++ b/cast_receiver_app/test/player_test.js @@ -0,0 +1,470 @@ +/** + * 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. + * + * @fileoverview Unit tests for playback methods. + */ + +goog.module('exoplayer.cast.test'); +goog.setTestOnly(); + +const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); +const Player = goog.require('exoplayer.cast.Player'); +const mocks = goog.require('exoplayer.cast.test.mocks'); +const testSuite = goog.require('goog.testing.testSuite'); +const util = goog.require('exoplayer.cast.test.util'); + +let player; +let shakaFake; + +testSuite({ + setUp() { + mocks.setUp(); + shakaFake = mocks.createShakaFake(); + player = new Player(shakaFake, new ConfigurationFactory()); + }, + + /** Tests the player initialisation */ + testPlayerInitialisation() { + mocks.state().isSilent = true; + const states = []; + let stateCounter = 0; + let currentState; + player.addPlayerListener((playerState) => { + states.push(playerState); + }); + + // Dump the initial state manually. + player.invalidate(); + stateCounter++; + assertEquals(stateCounter, states.length); + currentState = states[stateCounter - 1]; + assertEquals(0, currentState.mediaQueue.length); + assertEquals(0, currentState.windowIndex); + assertNull(currentState.playbackPosition); + + // Seek with uuid to prepare with later + const uuid = 'uuid1'; + player.seekToUuid(uuid, 30 * 1000); + stateCounter++; + assertEquals(stateCounter, states.length); + currentState = states[stateCounter - 1]; + assertEquals(30 * 1000, player.getCurrentPositionMs()); + assertEquals(0, player.getCurrentWindowIndex()); + assertEquals(-1, player.windowIndex_); + assertEquals(1, currentState.playbackPosition.periodId); + assertEquals(uuid, currentState.playbackPosition.uuid); + assertEquals(uuid, player.uuidToPrepare_); + + // Add a DASH media item. + player.addQueueItems(0, util.queue.slice(0, 2)); + stateCounter++; + assertEquals(stateCounter, states.length); + currentState = states[stateCounter - 1]; + assertEquals('IDLE', currentState.playbackState); + assertNotNull(currentState.playbackPosition); + util.assertUuidIndexMap(player.queueUuidIndexMap_, currentState.mediaQueue); + + // Prepare. + player.prepare(); + stateCounter++; + assertEquals(stateCounter, states.length); + currentState = states[stateCounter - 1]; + assertEquals(2, currentState.mediaQueue.length); + assertEquals('BUFFERING', currentState.playbackState); + assertEquals( + Player.DUMMY_MEDIA_ITEM_INFO, currentState.mediaItemsInfo[uuid]); + assertNull(player.uuidToPrepare_); + + // The video element starts waiting. + mocks.state().isSilent = false; + mocks.notifyListeners('waiting'); + // Nothing happens, masked buffering state after preparing. + assertEquals(stateCounter, states.length); + + // The manifest arrives. + mocks.notifyListeners('streaming'); + stateCounter++; + assertEquals(stateCounter, states.length); + currentState = states[stateCounter - 1]; + assertEquals(2, currentState.mediaQueue.length); + assertEquals('BUFFERING', currentState.playbackState); + assertEquals(uuid, currentState.playbackPosition.uuid); + assertEquals(0, currentState.playbackPosition.periodId); + assertEquals(30 * 1000, currentState.playbackPosition.positionMs); + // The dummy media item info has been replaced by the real one. + assertEquals(20000000, currentState.mediaItemsInfo[uuid].windowDurationUs); + assertEquals(0, currentState.mediaItemsInfo[uuid].defaultStartPositionUs); + assertEquals(0, currentState.mediaItemsInfo[uuid].positionInFirstPeriodUs); + assertTrue(currentState.mediaItemsInfo[uuid].isSeekable); + assertFalse(currentState.mediaItemsInfo[uuid].isDynamic); + + // Tracks have initially changed. + mocks.notifyListeners('trackschanged'); + // Nothing happens because the media item info remains the same. + assertEquals(stateCounter, states.length); + + // The video element reports the first frame rendered. + mocks.notifyListeners('loadeddata'); + stateCounter++; + assertEquals(stateCounter, states.length); + currentState = states[stateCounter - 1]; + assertEquals(2, currentState.mediaQueue.length); + assertEquals('READY', currentState.playbackState); + assertEquals(uuid, currentState.playbackPosition.uuid); + assertEquals(0, currentState.playbackPosition.periodId); + assertEquals(30 * 1000, currentState.playbackPosition.positionMs); + + // Playback starts. + mocks.notifyListeners('playing'); + // Nothing happens; we are ready already. + assertEquals(stateCounter, states.length); + + // Add another queue item. + player.addQueueItems(1, util.queue.slice(3, 4)); + stateCounter++; + assertEquals(stateCounter, states.length); + mocks.state().isSilent = true; + // Seek to the next queue item. + player.seekToWindow(1, 0); + stateCounter++; + assertEquals(stateCounter, states.length); + currentState = states[stateCounter - 1]; + const uuid1 = currentState.mediaQueue[1].uuid; + assertEquals( + Player.DUMMY_MEDIA_ITEM_INFO, currentState.mediaItemsInfo[uuid1]); + util.assertUuidIndexMap(player.queueUuidIndexMap_, currentState.mediaQueue); + + // The video element starts waiting. + mocks.state().isSilent = false; + mocks.notifyListeners('waiting'); + // Nothing happens, masked buffering state after preparing. + assertEquals(stateCounter, states.length); + + // The manifest arrives. + mocks.notifyListeners('streaming'); + stateCounter++; + assertEquals(stateCounter, states.length); + currentState = states[stateCounter - 1]; + // The dummy media item info has been replaced by the real one. + assertEquals(20000000, currentState.mediaItemsInfo[uuid].windowDurationUs); + assertEquals(0, currentState.mediaItemsInfo[uuid].defaultStartPositionUs); + assertEquals(0, currentState.mediaItemsInfo[uuid].positionInFirstPeriodUs); + assertTrue(currentState.mediaItemsInfo[uuid].isSeekable); + assertFalse(currentState.mediaItemsInfo[uuid].isDynamic); + }, + + /** Tests next and previous window when not yet prepared. */ + testNextPreviousWindow_notPrepared() { + assertEquals(-1, player.getNextWindowIndex()); + assertEquals(-1, player.getPreviousWindowIndex()); + player.addQueueItems(0, util.queue.slice(0, 2)); + assertEquals(-1, player.getNextWindowIndex()); + assertEquals(-1, player.getPreviousWindowIndex()); + }, + + /** Tests setting play when ready. */ + testPlayWhenReady() { + player.addQueueItems(0, util.queue.slice(0, 3)); + let playWhenReady = false; + player.addPlayerListener((state) => { + playWhenReady = state.playWhenReady; + }); + + assertEquals(false, player.getPlayWhenReady()); + assertEquals(false, playWhenReady); + + player.setPlayWhenReady(true); + assertEquals(true, player.getPlayWhenReady()); + assertEquals(true, playWhenReady); + + player.setPlayWhenReady(false); + assertEquals(false, player.getPlayWhenReady()); + assertEquals(false, playWhenReady); + }, + + /** Tests seeking to another position in the actual window. */ + async testSeek_inWindow() { + player.addQueueItems(0, util.queue.slice(0, 3)); + await player.seekToWindow(0, 1000); + + assertEquals(1, shakaFake.getMediaElement().currentTime); + assertEquals(1000, player.getCurrentPositionMs()); + assertEquals(0, player.getCurrentWindowIndex()); + }, + + /** Tests seeking to another window. */ + async testSeek_nextWindow() { + player.addQueueItems(0, util.queue.slice(0, 3)); + await player.prepare(); + assertEquals(util.queue[0].media.uri, shakaFake.getMediaElement().src); + assertEquals(-1, player.getPreviousWindowIndex()); + assertEquals(1, player.getNextWindowIndex()); + + player.seekToWindow(1, 2000); + assertEquals(0, player.getPreviousWindowIndex()); + assertEquals(2, player.getNextWindowIndex()); + assertEquals(2000, player.getCurrentPositionMs()); + assertEquals(1, player.getCurrentWindowIndex()); + assertEquals(util.queue[1].media.uri, mocks.state().loadedUri); + }, + + /** Tests the repeat mode 'none' */ + testRepeatMode_none() { + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + assertEquals(Player.RepeatMode.OFF, player.getRepeatMode()); + assertEquals(-1, player.getPreviousWindowIndex()); + assertEquals(1, player.getNextWindowIndex()); + + player.seekToWindow(2, 0); + assertEquals(1, player.getPreviousWindowIndex()); + assertEquals(-1, player.getNextWindowIndex()); + }, + + /** Tests the repeat mode 'all'. */ + testRepeatMode_all() { + let repeatMode; + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + player.addPlayerListener((state) => { + repeatMode = state.repeatMode; + }); + player.setRepeatMode(Player.RepeatMode.ALL); + assertEquals(Player.RepeatMode.ALL, repeatMode); + + player.seekToWindow(0,0); + assertEquals(2, player.getPreviousWindowIndex()); + assertEquals(1, player.getNextWindowIndex()); + + player.seekToWindow(2, 0); + assertEquals(1, player.getPreviousWindowIndex()); + assertEquals(0, player.getNextWindowIndex()); + }, + + /** + * Tests navigation within the queue when repeat mode and shuffle mode is on. + */ + testRepeatMode_all_inShuffleMode() { + const initialOrder = [2, 1, 0]; + let shuffleOrder; + let windowIndex; + player.addQueueItems(0, util.queue.slice(0, 3), initialOrder); + player.prepare(); + player.addPlayerListener((state) => { + shuffleOrder = state.shuffleOrder; + windowIndex = state.windowIndex; + }); + player.setRepeatMode(Player.RepeatMode.ALL); + player.setShuffleModeEnabled(true); + assertEquals(windowIndex, player.shuffleOrder_[player.shuffleIndex_]); + assertArrayEquals(initialOrder, shuffleOrder); + + player.seekToWindow(shuffleOrder[2], 0); + assertEquals(shuffleOrder[2], windowIndex); + assertEquals(shuffleOrder[0], player.getNextWindowIndex()); + assertEquals(shuffleOrder[1], player.getPreviousWindowIndex()); + + player.seekToWindow(shuffleOrder[0], 0); + assertEquals(shuffleOrder[0], windowIndex); + }, + + /** Tests the repeat mode 'one' */ + testRepeatMode_one() { + let repeatMode; + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + player.addPlayerListener((state) => { + repeatMode = state.repeatMode; + }); + player.setRepeatMode(Player.RepeatMode.ONE); + assertEquals(Player.RepeatMode.ONE, repeatMode); + assertEquals(0, player.getPreviousWindowIndex()); + assertEquals(0, player.getNextWindowIndex()); + + player.seekToWindow(1, 0); + assertEquals(1, player.getPreviousWindowIndex()); + assertEquals(1, player.getNextWindowIndex()); + + player.setShuffleModeEnabled(true); + assertEquals(1, player.getPreviousWindowIndex()); + assertEquals(1, player.getNextWindowIndex()); + }, + + /** Tests building a media item info from the manifest. */ + testBuildMediaItemInfo_fromManifest() { + let mediaItemInfos = null; + player.addQueueItems(0, util.queue.slice(0, 3)); + player.addPlayerListener((state) => { + mediaItemInfos = state.mediaItemsInfo; + }); + player.seekToWindow(1, 0); + player.prepare(); + assertUndefined(mediaItemInfos['uuid0']); + const mediaItemInfo = mediaItemInfos['uuid1']; + assertNotUndefined(mediaItemInfo); + assertFalse(mediaItemInfo.isDynamic); + assertTrue(mediaItemInfo.isSeekable); + assertEquals(0, mediaItemInfo.defaultStartPositionUs); + assertEquals(20 * 1000 * 1000, mediaItemInfo.windowDurationUs); + assertEquals(1, mediaItemInfo.periods.length); + assertEquals(20 * 1000 * 1000, mediaItemInfo.periods[0].durationUs); + }, + + /** Tests building a media item info with multiple periods. */ + testBuildMediaItemInfo_fromManifest_multiPeriod() { + let mediaItemInfos = null; + player.addQueueItems(0, util.queue.slice(0, 3)); + player.addPlayerListener((state) => { + mediaItemInfos = state.mediaItemsInfo; + }); + // Setting manifest properties to emulate a multiperiod stream manifest. + mocks.state().getManifest().periods.push({startTime: 20}); + mocks.state().manifestState.windowDuration = 50; + player.seekToWindow(1, 0); + player.prepare(); + + const mediaItemInfo = mediaItemInfos['uuid1']; + assertNotUndefined(mediaItemInfo); + assertFalse(mediaItemInfo.isDynamic); + assertTrue(mediaItemInfo.isSeekable); + assertEquals(0, mediaItemInfo.defaultStartPositionUs); + assertEquals(50 * 1000 * 1000, mediaItemInfo.windowDurationUs); + assertEquals(2, mediaItemInfo.periods.length); + assertEquals(20 * 1000 * 1000, mediaItemInfo.periods[0].durationUs); + assertEquals(30 * 1000 * 1000, mediaItemInfo.periods[1].durationUs); + }, + + /** Tests building a media item info from a live manifest. */ + testBuildMediaItemInfo_fromManifest_live() { + let mediaItemInfos = null; + player.addQueueItems(0, util.queue.slice(0, 3)); + player.addPlayerListener((state) => { + mediaItemInfos = state.mediaItemsInfo; + }); + // Setting manifest properties to emulate a live stream manifest. + mocks.state().manifestState.isLive = true; + mocks.state().manifestState.windowDuration = 30; + mocks.state().manifestState.delay = 10; + mocks.state().getManifest().periods.push({startTime: 20}); + player.seekToWindow(1, 0); + player.prepare(); + + const mediaItemInfo = mediaItemInfos['uuid1']; + assertNotUndefined(mediaItemInfo); + assertTrue(mediaItemInfo.isDynamic); + assertTrue(mediaItemInfo.isSeekable); + assertEquals(20 * 1000 * 1000, mediaItemInfo.defaultStartPositionUs); + assertEquals(20 * 1000 * 1000, mediaItemInfo.windowDurationUs); + assertEquals(2, mediaItemInfo.periods.length); + assertEquals(20 * 1000 * 1000, mediaItemInfo.periods[0].durationUs); + assertEquals(Infinity, mediaItemInfo.periods[1].durationUs); + }, + + /** Tests whether the shaka request filter is set for life streams. */ + testRequestFilterIsSetAndRemovedForLive() { + player.addQueueItems(0, util.queue.slice(0, 3)); + + // Set manifest properties to emulate a live stream manifest. + mocks.state().manifestState.isLive = true; + mocks.state().manifestState.windowDuration = 30; + mocks.state().manifestState.delay = 10; + mocks.state().getManifest().periods.push({startTime: 20}); + + assertNull(mocks.state().responseFilter); + assertFalse(player.isManifestFilterRegistered_); + player.seekToWindow(1, 0); + player.prepare(); + assertNotNull(mocks.state().responseFilter); + assertTrue(player.isManifestFilterRegistered_); + + // Set manifest properties to emulate a non-live stream */ + mocks.state().manifestState.isLive = false; + mocks.state().manifestState.windowDuration = 20; + mocks.state().manifestState.delay = 0; + mocks.state().getManifest().periods.push({startTime: 20}); + + player.seekToWindow(0, 0); + assertNull(mocks.state().responseFilter); + assertFalse(player.isManifestFilterRegistered_); + }, + + /** Tests whether the media info is removed when queue item is removed. */ + testRemoveMediaItemInfo() { + let mediaItemInfos = null; + player.addQueueItems(0, util.queue.slice(0, 3)); + player.addPlayerListener((state) => { + mediaItemInfos = state.mediaItemsInfo; + }); + player.seekToWindow(1, 0); + player.prepare(); + assertNotUndefined(mediaItemInfos['uuid1']); + player.removeQueueItems(['uuid1']); + assertUndefined(mediaItemInfos['uuid1']); + }, + + /** Tests shuffling. */ + testSetShuffeModeEnabled() { + let shuffleModeEnabled = false; + player.addQueueItems(0, util.queue.slice(0, 3)); + player.addPlayerListener((state) => { + shuffleModeEnabled = state.shuffleModeEnabled; + }); + player.setShuffleModeEnabled(true); + assertTrue(shuffleModeEnabled); + + player.setShuffleModeEnabled(false); + assertFalse(shuffleModeEnabled); + }, + + /** Tests setting a new playback order. */ + async testSetShuffleOrder() { + const defaultOrder = [0, 1, 2]; + let shuffleOrder; + player.addPlayerListener((state) => { + shuffleOrder = state.shuffleOrder; + }); + await player.addQueueItems(0, util.queue.slice(0, 3), defaultOrder); + assertArrayEquals(defaultOrder, shuffleOrder); + + player.setShuffleOrder_([2, 1, 0]); + assertArrayEquals([2, 1, 0], player.shuffleOrder_); + }, + + /** Tests setting a new playback order with incorrect length. */ + async testSetShuffleOrder_incorrectLength() { + const defaultOrder = [0, 1, 2]; + let shuffleOrder; + player.addPlayerListener((state) => { + shuffleOrder = state.shuffleOrder; + }); + await player.addQueueItems(0, util.queue.slice(0, 3), defaultOrder); + assertArrayEquals(defaultOrder, shuffleOrder); + + shuffleOrder = undefined; + player.setShuffleOrder_([2, 1]); + assertUndefined(shuffleOrder); + }, + + /** Tests falling into ENDED when prepared with empty queue. */ + testPrepare_withEmptyQueue() { + player.seekToUuid('uuid1000', 1000); + assertEquals('uuid1000', player.uuidToPrepare_); + player.prepare(); + assertEquals('ENDED', player.getPlaybackState()); + assertNull(player.uuidToPrepare_); + player.seekToUuid('uuid1000', 1000); + assertNull(player.uuidToPrepare_); + }, +}); diff --git a/cast_receiver_app/test/queue_test.js b/cast_receiver_app/test/queue_test.js new file mode 100644 index 0000000000..b46361fb2e --- /dev/null +++ b/cast_receiver_app/test/queue_test.js @@ -0,0 +1,166 @@ +/** + * 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. + * + * @fileoverview Unit tests for queue manipulations. + */ + +goog.module('exoplayer.cast.test.queue'); +goog.setTestOnly(); + +const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); +const Player = goog.require('exoplayer.cast.Player'); +const mocks = goog.require('exoplayer.cast.test.mocks'); +const testSuite = goog.require('goog.testing.testSuite'); +const util = goog.require('exoplayer.cast.test.util'); + +let player; + +testSuite({ + setUp() { + mocks.setUp(); + player = new Player(mocks.createShakaFake(), new ConfigurationFactory()); + }, + + /** Tests adding queue items. */ + testAddQueueItem() { + let queue = []; + player.addPlayerListener((state) => { + queue = state.mediaQueue; + }); + assertEquals(0, queue.length); + player.addQueueItems(0, util.queue.slice(0, 3)); + assertEquals(util.queue[0].media.uri, queue[0].media.uri); + assertEquals(util.queue[1].media.uri, queue[1].media.uri); + assertEquals(util.queue[2].media.uri, queue[2].media.uri); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests that duplicate queue items are ignored. */ + testAddDuplicateQueueItem() { + let queue = []; + player.addPlayerListener((state) => { + queue = state.mediaQueue; + }); + assertEquals(0, queue.length); + // Insert three items. + player.addQueueItems(0, util.queue.slice(0, 3)); + // Insert two of which the first is a duplicate. + player.addQueueItems(1, util.queue.slice(2, 4)); + assertEquals(4, queue.length); + assertArrayEquals( + ['uuid0', 'uuid3', 'uuid1', 'uuid2'], queue.slice().map((i) => i.uuid)); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests moving queue items. */ + testMoveQueueItem() { + const shuffleOrder = [0, 2, 1]; + let queue = []; + player.addPlayerListener((state) => { + queue = state.mediaQueue; + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.moveQueueItem('uuid0', 1, shuffleOrder); + assertEquals(util.queue[1].media.uri, queue[0].media.uri); + assertEquals(util.queue[0].media.uri, queue[1].media.uri); + assertEquals(util.queue[2].media.uri, queue[2].media.uri); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + + queue = undefined; + // invalid to index + player.moveQueueItem('uuid0', 11, [0, 1, 2]); + assertTrue(typeof queue === 'undefined'); + assertArrayEquals(shuffleOrder, player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); + // negative to index + player.moveQueueItem('uuid0', -11, shuffleOrder); + assertTrue(typeof queue === 'undefined'); + assertArrayEquals(shuffleOrder, player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); + // unknown uuid + player.moveQueueItem('unknown', 1, shuffleOrder); + assertTrue(typeof queue === 'undefined'); + assertArrayEquals(shuffleOrder, player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); + }, + + /** Tests removing queue items. */ + testRemoveQueueItems() { + let queue = []; + player.addPlayerListener((state) => { + queue = state.mediaQueue; + }); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + player.prepare(); + player.seekToWindow(1, 0); + assertEquals(1, player.getCurrentWindowIndex()); + util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); + + // Remove the first item. + player.removeQueueItems(['uuid0']); + assertEquals(2, queue.length); + assertEquals(util.queue[1].media.uri, queue[0].media.uri); + assertEquals(util.queue[2].media.uri, queue[1].media.uri); + assertEquals(0, player.getCurrentWindowIndex()); + assertArrayEquals([1,0], player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); + + // Calling stop without reseting preserves the queue. + player.stop(false); + assertEquals('uuid1', player.uuidToPrepare_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); + + // Remove the item at the end of the queue. + player.removeQueueItems(['uuid2']); + util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); + + // Remove the last remaining item in the queue. + player.removeQueueItems(['uuid1']); + assertEquals(0, queue.length); + assertEquals('IDLE', player.getPlaybackState()); + assertEquals(0, player.getCurrentWindowIndex()); + assertArrayEquals([], player.shuffleOrder_); + assertNull(player.uuidToPrepare_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); + }, + + /** Tests removing multiple unordered queue items at once. */ + testRemoveQueueItems_multiple() { + let queue = []; + player.addPlayerListener((state) => { + queue = state.mediaQueue; + }); + player.addQueueItems(0, util.queue.slice(0, 6), []); + player.prepare(); + + assertEquals(6, queue.length); + player.removeQueueItems(['uuid1', 'uuid5', 'uuid3']); + assertArrayEquals(['uuid0', 'uuid2', 'uuid4'], queue.map((i) => i.uuid)); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests whether stopping with reset=true resets queue and uuidToIndexMap */ + testStop_resetTrue() { + let queue = []; + player.addPlayerListener((state) => { + queue = state.mediaQueue; + }); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + player.prepare(); + player.stop(true); + assertEquals(0, player.queue_.length); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, +}); diff --git a/cast_receiver_app/test/receiver_test.js b/cast_receiver_app/test/receiver_test.js new file mode 100644 index 0000000000..303a1caf64 --- /dev/null +++ b/cast_receiver_app/test/receiver_test.js @@ -0,0 +1,1027 @@ +/** + * 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. + * + * @fileoverview Unit tests for receiver. + */ + +goog.module('exoplayer.cast.test.receiver'); +goog.setTestOnly(); + +const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); +const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher'); +const Player = goog.require('exoplayer.cast.Player'); +const Receiver = goog.require('exoplayer.cast.Receiver'); +const mocks = goog.require('exoplayer.cast.test.mocks'); +const testSuite = goog.require('goog.testing.testSuite'); +const util = goog.require('exoplayer.cast.test.util'); + +/** @type {?Player|undefined} */ +let player; +/** @type {!Array} */ +let queue = []; +let shakaFake; +let castContextMock; + +/** + * Sends a message to the receiver under test. + * + * @param {!Object} message The message to send as json. + */ +const sendMessage = function(message) { + mocks.state().customMessageListener({ + data: message, + senderId: 'sender0', + }); +}; + +/** + * Creates a valid media item with the suffix appended to each field. + * + * @param {string} suffix The suffix to append to the fields value. + * @return {!Object} The media item. + */ +const createMediaItem = function(suffix) { + return { + uuid: 'uuid' + suffix, + media: {uri: 'uri' + suffix}, + mimeType: 'application/dash+xml', + }; +}; + +let messageSequence = 0; + +/** + * Creates a message in the format sent bey the sender app. + * + * @param {string} method The name of the method. + * @param {?Object} args The arguments. + * @return {!Object} The message. + */ +const createMessage = function (method, args) { + return { + method: method, + args: args, + sequenceNumber: ++messageSequence, + }; +}; + +/** + * Asserts the `playerState` is in the same state as just after creation of the + * player. + * + * @param {!PlayerState} playerState The player state to assert. + * @param {string} playbackState The expected playback state. + */ +const assertInitialState = function(playerState, playbackState) { + assertEquals(playbackState, playerState.playbackState); + // Assert the state is in initial state. + assertArrayEquals([], queue); + assertEquals(0, playerState.windowCount); + assertEquals(0, playerState.windowIndex); + assertUndefined(playerState.playbackError); + assertNull(playerState.playbackPosition); + // Assert player properties. + assertEquals(0, player.getDurationMs()); + assertArrayEquals([], Object.entries(player.mediaItemInfoMap_)); + assertEquals(0, player.windowPeriodIndex_); + assertEquals(999, player.playbackType_); + assertEquals(0, player.getCurrentWindowIndex()); + assertEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); +}; + + +testSuite({ + setUp() { + mocks.setUp(); + shakaFake = mocks.createShakaFake(); + castContextMock = mocks.createCastReceiverContextFake(); + player = new Player(shakaFake, new ConfigurationFactory()); + player.addPlayerListener((playerState) => { + queue = playerState.mediaQueue; + }); + const messageDispatcher = new MessageDispatcher( + 'urn:x-cast:com.google.exoplayer.cast', castContextMock); + new Receiver(player, castContextMock, messageDispatcher); + }, + + tearDown() { + queue = []; + }, + + /** Tests whether a status was sent to the sender on connect. */ + testNotifyClientConnected() { + assertUndefined(mocks.state().outputMessages['sender0']); + + sendMessage(createMessage('player.onClientConnected', {})); + const message = mocks.state().outputMessages['sender0'][0]; + assertEquals(messageSequence, message.sequenceNumber); + }, + + /** + * Tests whether a custom message listener has been registered after + * construction. + */ + testCustomMessageListener() { + assertTrue(goog.isFunction(mocks.state().customMessageListener)); + }, + + /** Tests set playWhenReady. */ + testSetPlayWhenReady() { + let playWhenReady; + player.addPlayerListener((playerState) => { + playWhenReady = playerState.playWhenReady; + }); + + sendMessage(createMessage( + 'player.setPlayWhenReady', + { playWhenReady: true } + )); + assertTrue(playWhenReady); + sendMessage(createMessage( + 'player.setPlayWhenReady', + { playWhenReady: false } + )); + assertFalse(playWhenReady); + }, + + /** Tests setting repeat modes. */ + testSetRepeatMode() { + let repeatMode; + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + player.addPlayerListener((playerState) => { + repeatMode = playerState.repeatMode; + }); + + sendMessage(createMessage( + 'player.setRepeatMode', + { repeatMode: Player.RepeatMode.ONE } + )); + assertEquals(Player.RepeatMode.ONE, repeatMode); + assertEquals(0, player.getNextWindowIndex()); + assertEquals(0, player.getPreviousWindowIndex()); + + sendMessage(createMessage( + 'player.setRepeatMode', + { repeatMode: Player.RepeatMode.ALL } + )); + assertEquals(Player.RepeatMode.ALL, repeatMode); + assertEquals(1, player.getNextWindowIndex()); + assertEquals(2, player.getPreviousWindowIndex()); + + sendMessage(createMessage( + 'player.setRepeatMode', + { repeatMode: Player.RepeatMode.OFF } + )); + assertEquals(Player.RepeatMode.OFF, repeatMode); + assertEquals(1, player.getNextWindowIndex()); + assertTrue(player.getPreviousWindowIndex() < 0); + }, + + /** Tests setting an invalid repeat mode value. */ + testSetRepeatMode_invalid_noStateChange() { + let repeatMode; + player.addPlayerListener((playerState) => { + repeatMode = playerState.repeatMode; + }); + + sendMessage(createMessage( + 'player.setRepeatMode', + { repeatMode: "UNKNOWN" } + )); + assertEquals(Player.RepeatMode.OFF, player.repeatMode_); + assertUndefined(repeatMode); + player.invalidate(); + assertEquals(Player.RepeatMode.OFF, repeatMode); + }, + + /** Tests enabling and disabling shuffle mode. */ + testSetShuffleModeEnabled() { + const enableMessage = createMessage('player.setShuffleModeEnabled', { + shuffleModeEnabled: true, + }); + const disableMessage = createMessage('player.setShuffleModeEnabled', { + shuffleModeEnabled: false, + }); + let shuffleModeEnabled; + player.addPlayerListener((state) => { + shuffleModeEnabled = state.shuffleModeEnabled; + }); + assertFalse(player.shuffleModeEnabled_); + sendMessage(enableMessage); + assertTrue(shuffleModeEnabled); + sendMessage(disableMessage); + assertFalse(shuffleModeEnabled); + }, + + /** Tests adding a single media item to the queue. */ + testAddMediaItem_single() { + const suffix = '0'; + const jsonMessage = createMessage('player.addItems', { + index: 0, + items: [ + createMediaItem(suffix), + ], + shuffleOrder: [0], + }); + + sendMessage(jsonMessage); + assertEquals(1, queue.length); + assertEquals('uuid0', queue[0].uuid); + assertEquals('uri0', queue[0].media.uri); + assertArrayEquals([0], player.shuffleOrder_); + }, + + /** Tests adding multiple media items to the queue. */ + testAddMediaItem_multiple() { + const shuffleOrder = [0, 2, 1]; + const jsonMessage = createMessage('player.addItems', { + index: 0, + items: [ + createMediaItem('0'), + createMediaItem('1'), + createMediaItem('2'), + ], + shuffleOrder: shuffleOrder, + }); + + sendMessage(jsonMessage); + assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); + assertArrayEquals(shuffleOrder, player.shuffleOrder_); + }, + + /** Tests adding a media item to end of the queue by omitting the index. */ + testAddMediaItem_noindex_addstoend() { + const shuffleOrder = [1, 3, 2, 0]; + const jsonMessage = createMessage('player.addItems', { + items: [createMediaItem('99')], + shuffleOrder: shuffleOrder, + }); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + let queue = []; + player.addPlayerListener((playerState) => { + queue = playerState.mediaQueue; + }); + sendMessage(jsonMessage); + assertEquals(4, queue.length); + assertEquals('uuid99', queue[3].uuid); + assertArrayEquals(shuffleOrder, player.shuffleOrder_); + }, + + /** Tests adding items with a shuffle order of invalid length. */ + testAddMediaItems_invalidShuffleOrderLength() { + const shuffleOrder = [1, 3, 2]; + const jsonMessage = createMessage('player.addItems', { + items: [createMediaItem('99')], + shuffleOrder: shuffleOrder, + }); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + let queue = []; + player.addPlayerListener((playerState) => { + queue = playerState.mediaQueue; + }); + sendMessage(jsonMessage); + assertEquals(4, queue.length); + assertEquals('uuid99', queue[3].uuid); + assertEquals(4, player.shuffleOrder_.length); + }, + + /** Tests inserting a media item to the queue. */ + testAddMediaItem_insert() { + const index = 1; + const shuffleOrder = [1, 0, 3, 2, 4]; + const firstInsertionMessage = createMessage('player.addItems', { + index, + items: [ + createMediaItem('99'), + createMediaItem('100'), + ], + shuffleOrder, + }); + const prepareMessage = createMessage('player.prepare', {}); + const secondInsertionMessage = createMessage('player.addItems', { + index, + items: [ + createMediaItem('199'), + createMediaItem('1100'), + ], + shuffleOrder, + }); + // fill with three items + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + player.seekToUuid('uuid99', 0); + + sendMessage(firstInsertionMessage); + // The window index does not change when IDLE. + assertEquals(1, player.getCurrentWindowIndex()); + assertEquals(5, queue.length); + assertArrayEquals(shuffleOrder, player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + + // Prepare sets the index by the uuid to which we seeked. + sendMessage(prepareMessage); + assertEquals(1, player.getCurrentWindowIndex()); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + // Add two items at the current window index. + sendMessage(secondInsertionMessage); + // Current window index is adjusted. + assertEquals(3, player.getCurrentWindowIndex()); + assertEquals(7, queue.length); + assertEquals('uuid199', queue[index].uuid); + assertEquals(7, player.shuffleOrder_.length); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests adding a media item with an index larger than the queue size. */ + testAddMediaItem_indexLargerThanQueueSize_addsToEnd() { + const index = 4; + const jsonMessage = createMessage('player.addItems', { + index: index, + items: [ + createMediaItem('99'), + createMediaItem('100'), + ], + shuffleOrder: [], + }); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + + assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); + sendMessage(jsonMessage); + assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid99', 'uuid100'], + queue.map((x) => x.uuid)); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests removing an item from the queue. */ + testRemoveMediaItem() { + const jsonMessage = + createMessage('player.removeItems', {uuids: ['uuid1', 'uuid0']}); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); + + sendMessage(jsonMessage); + assertArrayEquals(['uuid2'], queue.map((x) => x.uuid)); + assertArrayEquals([0], player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests removing the currently playing item from the queue. */ + async testRemoveMediaItem_currentItem() { + const jsonMessage = + createMessage('player.removeItems', {uuids: ['uuid1', 'uuid0']}); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + player.seekToWindow(1, 0); + player.prepare(); + + await sendMessage(jsonMessage); + assertArrayEquals(['uuid2'], queue.map((x) => x.uuid)); + assertEquals(0, player.getCurrentWindowIndex()); + assertEquals(util.queue[2].media.uri, shakaFake.getMediaElement().src); + assertArrayEquals([0], player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests removing items which affect the current window index. */ + async testRemoveMediaItem_affectsWindowIndex() { + const jsonMessage = + createMessage('player.removeItems', {uuids: ['uuid1', 'uuid0']}); + const currentUri = util.queue[4].media.uri; + player.addQueueItems(0, util.queue.slice(0, 6), [3, 2, 1, 4, 0, 5]); + player.prepare(); + await player.seekToWindow(4, 2000); + assertEquals(currentUri, shakaFake.getMediaElement().src); + + sendMessage(jsonMessage); + assertEquals(4, queue.length); + assertEquals('uuid4', queue[player.getCurrentWindowIndex()].uuid); + assertEquals(2, player.getCurrentWindowIndex()); + assertEquals(currentUri, shakaFake.getMediaElement().src); + assertArrayEquals([1, 0, 2, 3], player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests removing the last item of the queue. */ + testRemoveMediaItem_firstItem_windowIndexIsCorrect() { + const jsonMessage = + createMessage('player.removeItems', {uuids: ['uuid0']}); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + player.seekToWindow(1, 0); + + sendMessage(jsonMessage); + assertArrayEquals(['uuid1', 'uuid2'], queue.map((x) => x.uuid)); + assertEquals(0, player.getCurrentWindowIndex()); + assertArrayEquals([1, 0], player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests removing the last item of the queue. */ + testRemoveMediaItem_lastItem_windowIndexIsCorrect() { + const jsonMessage = + createMessage('player.removeItems', {uuids: ['uuid2']}); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + player.seekToWindow(2, 0); + player.prepare(); + + mocks.state().isSilent = true; + const states = []; + player.addPlayerListener((playerState) => { + states.push(playerState); + }); + sendMessage(jsonMessage); + assertArrayEquals(['uuid0', 'uuid1'], queue.map((x) => x.uuid)); + assertEquals(1, player.getCurrentWindowIndex()); + assertArrayEquals([0, 1], player.shuffleOrder_); + assertEquals(1, states.length); + assertEquals(Player.PlaybackState.BUFFERING, states[0].playbackState); + assertEquals( + Player.DiscontinuityReason.PERIOD_TRANSITION, + states[0].playbackPosition.discontinuityReason); + assertEquals( + Player.DUMMY_MEDIA_ITEM_INFO, states[0].mediaItemsInfo['uuid1']); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests removing items all items. */ + testRemoveMediaItem_removeAll() { + const jsonMessage = createMessage('player.removeItems', + {uuids: ['uuid1', 'uuid0', 'uuid2']}); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.seekToWindow(2, 2000); + player.prepare(); + let playerState; + player.addPlayerListener((state) => { + playerState = state; + }); + + sendMessage(jsonMessage); + assertInitialState(playerState, 'ENDED'); + assertEquals(0, player.getCurrentWindowIndex()); + assertArrayEquals([], player.shuffleOrder_); + assertEquals(Player.PlaybackState.ENDED, player.getPlaybackState()); + util.assertUuidIndexMap(player.queueUuidIndexMap_, []); + }, + + /** Tests moving an item in the queue. */ + testMoveItem() { + let shuffleOrder = [0, 2, 1]; + const jsonMessage = createMessage('player.moveItem', { + uuid: 'uuid2', + index: 0, + shuffleOrder: shuffleOrder, + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + + assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); + sendMessage(jsonMessage); + assertArrayEquals(['uuid2', 'uuid0', 'uuid1'], queue.map((x) => x.uuid)); + assertArrayEquals(shuffleOrder, player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests moving the currently playing item in the queue. */ + testMoveItem_currentWindowIndex() { + let shuffleOrder = [0, 2, 1]; + const jsonMessage = createMessage('player.moveItem', { + uuid: 'uuid2', + index: 0, + shuffleOrder: shuffleOrder, + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + player.seekToUuid('uuid2', 0); + assertEquals(2, player.getCurrentWindowIndex()); + + assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); + sendMessage(jsonMessage); + assertArrayEquals(['uuid2', 'uuid0', 'uuid1'], queue.map((x) => x.uuid)); + assertEquals(0, player.getCurrentWindowIndex()); + assertArrayEquals(shuffleOrder, player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests moving an item from before to after the currently playing item. */ + testMoveItem_decreaseCurrentWindowIndex() { + const jsonMessage = createMessage('player.moveItem', { + uuid: 'uuid0', + index: 5, + shuffleOrder: [], + }); + player.addQueueItems(0, util.queue.slice(0, 6)); + player.prepare(); + player.seekToWindow(2, 0); + assertEquals(2, player.getCurrentWindowIndex()); + + assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], + queue.map((x) => x.uuid)); + sendMessage(jsonMessage); + assertArrayEquals(['uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5', 'uuid0'], + queue.map((x) => x.uuid)); + assertEquals(1, player.getCurrentWindowIndex()); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests moving an item from after to before the currently playing item. */ + testMoveItem_increaseCurrentWindowIndex() { + const jsonMessage = createMessage('player.moveItem', { + uuid: 'uuid5', + index: 0, + shuffleOrder: [], + }); + player.addQueueItems(0, util.queue.slice(0, 6)); + player.prepare(); + player.seekToWindow(2, 0); + assertEquals(2, player.getCurrentWindowIndex()); + + assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], + queue.map((x) => x.uuid)); + sendMessage(jsonMessage); + assertArrayEquals(['uuid5', 'uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4'], + queue.map((x) => x.uuid)); + assertEquals(3, player.getCurrentWindowIndex()); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests moving an item from after to the current window index. */ + testMoveItem_toCurrentWindowIndex_fromAfter() { + const jsonMessage = createMessage('player.moveItem', { + uuid: 'uuid5', + index: 2, + shuffleOrder: [], + }); + player.addQueueItems(0, util.queue.slice(0, 6)); + player.prepare(); + player.seekToWindow(2, 0); + assertEquals(2, player.getCurrentWindowIndex()); + + assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], + queue.map((x) => x.uuid)); + sendMessage(jsonMessage); + assertArrayEquals(['uuid0', 'uuid1', 'uuid5', 'uuid2', 'uuid3', 'uuid4'], + queue.map((x) => x.uuid)); + assertEquals(3, player.getCurrentWindowIndex()); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests moving an item from before to the current window index. */ + testMoveItem_toCurrentWindowIndex_fromBefore() { + const jsonMessage = createMessage('player.moveItem', { + uuid: 'uuid0', + index: 2, + shuffleOrder: [], + }); + player.addQueueItems(0, util.queue.slice(0, 6)); + player.prepare(); + player.seekToWindow(2, 0); + assertEquals(2, player.getCurrentWindowIndex()); + + assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], + queue.map((x) => x.uuid)); + sendMessage(jsonMessage); + assertArrayEquals(['uuid1', 'uuid2', 'uuid0', 'uuid3', 'uuid4', 'uuid5'], + queue.map((x) => x.uuid)); + assertEquals(1, player.getCurrentWindowIndex()); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests seekTo. */ + testSeekTo() { + const jsonMessage = createMessage('player.seekTo', + { + 'uuid': 'uuid1', + 'positionMs': 2000 + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + sendMessage(jsonMessage); + assertEquals(2000, player.getCurrentPositionMs()); + assertEquals(1, player.getCurrentWindowIndex()); + }, + + /** Tests seekTo to unknown uuid. */ + testSeekTo_unknownUuid() { + const jsonMessage = createMessage('player.seekTo', + { + 'uuid': 'unknown', + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + player.seekToWindow(1, 2000); + assertEquals(2000, player.getCurrentPositionMs()); + assertEquals(1, player.getCurrentWindowIndex()); + + sendMessage(jsonMessage); + assertEquals(2000, player.getCurrentPositionMs()); + assertEquals(1, player.getCurrentWindowIndex()); + }, + + /** Tests seekTo without position. */ + testSeekTo_noPosition_defaultsToZero() { + const jsonMessage = createMessage('player.seekTo', + { + 'uuid': 'uuid1', + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + sendMessage(jsonMessage); + assertEquals(0, player.getCurrentPositionMs()); + assertEquals(1, player.getCurrentWindowIndex()); + }, + + /** Tests seekTo to negative position. */ + testSeekTo_negativePosition_defaultsToZero() { + const jsonMessage = createMessage('player.seekTo', + { + 'uuid': 'uuid2', + 'positionMs': -1, + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + player.seekToWindow(1, 2000); + assertEquals(2000, player.getCurrentPositionMs()); + assertEquals(1, player.getCurrentWindowIndex()); + + sendMessage(jsonMessage); + assertEquals(0, player.getCurrentPositionMs()); + assertEquals(2, player.getCurrentWindowIndex()); + }, + + /** Tests whether validation is turned on. */ + testMediaItemValidation_isOn() { + const index = 0; + const mediaItem = createMediaItem('99'); + delete mediaItem.uuid; + const jsonMessage = createMessage('player.addItems', { + index: index, + items: [mediaItem], + shuffleOrder: [], + }); + + sendMessage(jsonMessage); + assertEquals(0, queue.length); + }, + + /** Tests whether the state is sent to sender apps on state transition. */ + testPlayerStateIsSent_withCorrectSequenceNumber() { + assertUndefined(mocks.state().outputMessages['sender0']); + const playMessage = + createMessage('player.setPlayWhenReady', {playWhenReady: true}); + sendMessage(playMessage); + + const playerState = mocks.state().outputMessages['sender0'][0]; + assertTrue(playerState.playWhenReady); + assertEquals(playMessage.sequenceNumber, playerState.sequenceNumber); + }, + + /** Tests whether a connect of a sender app sends the current player state. */ + testSenderConnection() { + const onSenderConnected = mocks.state().onSenderConnected; + assertTrue(goog.isFunction(onSenderConnected)); + onSenderConnected({senderId: 'sender0'}); + + const playerState = mocks.state().outputMessages['sender0'][0]; + assertEquals(Player.RepeatMode.OFF, playerState.repeatMode); + assertEquals('IDLE', playerState.playbackState); + assertArrayEquals([], playerState.mediaQueue); + assertEquals(-1, playerState.sequenceNumber); + }, + + /** Tests whether a disconnect of a sender notifies the message dispatcher. */ + testSenderDisconnection_callsMessageDispatcher() { + mocks.setUp(); + let notifiedSenderId; + const myPlayer = new Player(mocks.createShakaFake()); + const myManagerFake = mocks.createCastReceiverContextFake(); + new Receiver(myPlayer, myManagerFake, { + registerActionHandler() {}, + notifySenderDisconnected(senderId) { + notifiedSenderId = senderId; + }, + }); + + const onSenderDisconnected = mocks.state().onSenderDisconnected; + assertTrue(goog.isFunction(onSenderDisconnected)); + onSenderDisconnected({senderId: 'sender0'}); + assertEquals('sender0', notifiedSenderId); + }, + + /** + * Tests whether the state right after creation of the player matches + * expectations. + */ + testInitialState() { + mocks.state().isSilent = true; + let playerState; + player.addPlayerListener((state) => { + playerState = state; + }); + assertEquals(0, player.getCurrentPositionMs()); + // Dump a player state to the listener. + player.invalidate(); + // Asserts the state just after creation. + assertInitialState(playerState, 'IDLE'); + }, + + /** Tests whether user properties can be changed when in IDLE state */ + testChangingUserPropertiesWhenIdle() { + mocks.state().isSilent = true; + const states = []; + let counter = 0; + player.addPlayerListener((state) => { + states.push(state); + }); + // Adding items when IDLE. + player.addQueueItems(0, util.queue.slice(0, 3)); + counter++; + assertEquals(counter, states.length); + assertEquals(Player.PlaybackState.IDLE, states[counter - 1].playbackState); + assertArrayEquals( + ['uuid0', 'uuid1', 'uuid2'], + states[counter - 1].mediaQueue.map((i) => i.uuid)); + + // Set playWhenReady when IDLE. + assertFalse(player.getPlayWhenReady()); + player.setPlayWhenReady(true); + counter++; + assertTrue(player.getPlayWhenReady()); + assertEquals(counter, states.length); + assertEquals(Player.PlaybackState.IDLE, states[counter - 1].playbackState); + + // Seeking when IDLE. + player.seekToUuid('uuid2', 1000); + counter++; + // Window index not set when idle. + assertEquals(2, player.getCurrentWindowIndex()); + assertEquals(1000, player.getCurrentPositionMs()); + assertEquals(counter, states.length); + assertEquals(Player.PlaybackState.IDLE, states[counter - 1].playbackState); + // But window index is set when prepared. + player.prepare(); + assertEquals(2, player.getCurrentWindowIndex()); + }, + + /** Tests the state after calling prepare. */ + testPrepare() { + mocks.state().isSilent = true; + const states = []; + let counter = 0; + player.addPlayerListener((state) => { + states.push(state); + }); + const prepareMessage = createMessage('player.prepare', {}); + + player.addQueueItems(0, util.queue.slice(0, 3)); + player.seekToWindow(1, 1000); + counter += 2; + + // Sends prepare message. + sendMessage(prepareMessage); + counter++; + assertEquals(counter, states.length); + assertEquals('uuid1', states[counter - 1].playbackPosition.uuid); + assertEquals( + Player.PlaybackState.BUFFERING, states[counter - 1].playbackState); + + // Fakes Shaka events. + mocks.state().isSilent = false; + mocks.notifyListeners('streaming'); + mocks.notifyListeners('loadeddata'); + counter += 2; + assertEquals(counter, states.length); + assertEquals(Player.PlaybackState.READY, states[counter - 1].playbackState); + }, + + /** Tests stopping the player with `reset=true`. */ + testStop_resetTrue() { + mocks.state().isSilent = true; + let playerState; + player.addPlayerListener((state) => { + playerState = state; + }); + const stopMessage = createMessage('player.stop', {reset: true}); + + player.setRepeatMode(Player.RepeatMode.ALL); + player.setShuffleModeEnabled(true); + player.setPlayWhenReady(true); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + mocks.state().isSilent = false; + mocks.notifyListeners('loadeddata'); + assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((i) => i.uuid)); + assertEquals(0, playerState.windowIndex); + assertNotEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); + assertEquals(1, player.playbackType_); + // Stop the player. + sendMessage(stopMessage); + // Asserts the state looks the same as just after creation. + assertInitialState(playerState, 'IDLE'); + assertNull(playerState.playbackPosition); + // Assert player properties are preserved. + assertTrue(playerState.shuffleModeEnabled); + assertTrue(playerState.playWhenReady); + assertEquals(Player.RepeatMode.ALL, playerState.repeatMode); + }, + + /** Tests stopping the player with `reset=false`. */ + testStop_resetFalse() { + mocks.state().isSilent = true; + let playerState; + player.addPlayerListener((state) => { + playerState = state; + }); + const stopMessage = createMessage('player.stop', {reset: false}); + + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + player.seekToUuid('uuid1', 1000); + mocks.state().isSilent = false; + mocks.notifyListeners('streaming'); + mocks.notifyListeners('trackschanged'); + mocks.notifyListeners('loadeddata'); + assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((i) => i.uuid)); + assertEquals(1, playerState.windowIndex); + assertNotEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); + assertEquals(2, player.playbackType_); + // Stop the player. + sendMessage(stopMessage); + assertEquals('IDLE', playerState.playbackState); + assertUndefined(playerState.playbackError); + // Assert the timeline is preserved. + assertEquals(3, queue.length); + assertEquals(3, playerState.windowCount); + assertEquals(1, player.windowIndex_); + assertEquals(1, playerState.windowIndex); + // Assert the playback position is correct. + assertEquals(1000, playerState.playbackPosition.positionMs); + assertEquals('uuid1', playerState.playbackPosition.uuid); + assertEquals(0, playerState.playbackPosition.periodId); + assertNull(playerState.playbackPosition.discontinuityReason); + assertEquals(1000, player.getCurrentPositionMs()); + // Assert player properties are preserved. + assertEquals(20000, player.getDurationMs()); + assertEquals(2, Object.entries(player.mediaItemInfoMap_).length); + assertEquals(0, player.windowPeriodIndex_); + assertEquals(1, player.getCurrentWindowIndex()); + assertEquals(1, player.windowIndex_); + assertNotEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); + assertEquals(999, player.playbackType_); + assertEquals('uuid1', player.uuidToPrepare_); + }, + + /** + * Tests the state after having removed the last item in the queue. This + * resolves to the same state like calling `stop(true)` except that the state + * is ENDED and the queue is naturally empty and hence the windowIndex is + * unset. + */ + testRemoveLastQueueItem() { + mocks.state().isSilent = true; + let playerState; + player.addPlayerListener((state) => { + playerState = state; + }); + const removeAllItemsMessage = createMessage( + 'player.removeItems', {uuids: ['uuid0', 'uuid1', 'uuid2']}); + + player.addQueueItems(0, util.queue.slice(0, 3)); + player.seekToWindow(0, 1000); + player.prepare(); + mocks.state().isSilent = false; + mocks.notifyListeners('loadeddata'); + // Remove all items. + sendMessage(removeAllItemsMessage); + // Assert the state after removal of all items. + assertInitialState(playerState, 'ENDED'); + }, + + /** Tests whether a player state is sent when no item is added. */ + testAddItem_noop() { + mocks.state().isSilent = true; + let playerStates = []; + player.addPlayerListener((state) => { + playerStates.push(state); + }); + const noOpMessage = createMessage('player.addItems', { + index: 0, + items: [ + util.queue[0], + ], + shuffleOrder: [0], + }); + player.addQueueItems(0, [util.queue[0]], []); + player.prepare(); + assertEquals(2, playerStates.length); + assertEquals(2, mocks.state().outputMessages['sender0'].length); + sendMessage(noOpMessage); + assertEquals(2, playerStates.length); + assertEquals(3, mocks.state().outputMessages['sender0'].length); + }, + + /** Tests whether a player state is sent when no item is removed. */ + testRemoveItem_noop() { + mocks.state().isSilent = true; + let playerStates = []; + player.addPlayerListener((state) => { + playerStates.push(state); + }); + const noOpMessage = + createMessage('player.removeItems', {uuids: ['uuid00']}); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + assertEquals(2, playerStates.length); + assertEquals(2, mocks.state().outputMessages['sender0'].length); + sendMessage(noOpMessage); + assertEquals(2, playerStates.length); + assertEquals(3, mocks.state().outputMessages['sender0'].length); + }, + + /** Tests whether a player state is sent when item is not moved. */ + testMoveItem_noop() { + mocks.state().isSilent = true; + let playerStates = []; + player.addPlayerListener((state) => { + playerStates.push(state); + }); + const noOpMessage = createMessage('player.moveItem', { + uuid: 'uuid00', + index: 0, + shuffleOrder: [], + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + assertEquals(2, playerStates.length); + assertEquals(2, mocks.state().outputMessages['sender0'].length); + sendMessage(noOpMessage); + assertEquals(2, playerStates.length); + assertEquals(3, mocks.state().outputMessages['sender0'].length); + }, + + /** Tests whether playback actions send a state when no-op */ + testNoOpPlaybackActionsSendPlayerState() { + mocks.state().isSilent = true; + let playerStates = []; + let parsedMessage; + player.addPlayerListener((state) => { + playerStates.push(state); + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + + const outputMessages = mocks.state().outputMessages['sender0']; + const setupMessageCount = playerStates.length; + let totalMessageCount = setupMessageCount; + assertEquals(setupMessageCount, playerStates.length); + assertEquals(totalMessageCount, outputMessages.length); + + const firstNoOpMessage = createMessage('player.setPlayWhenReady', { + playWhenReady: false, + }); + let expectedSequenceNumber = firstNoOpMessage.sequenceNumber; + + sendMessage(firstNoOpMessage); + totalMessageCount++; + assertEquals(setupMessageCount, playerStates.length); + assertEquals(totalMessageCount, outputMessages.length); + parsedMessage = outputMessages[totalMessageCount - 1]; + assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); + + sendMessage(createMessage('player.setRepeatMode', { + repeatMode: 'OFF', + })); + totalMessageCount++; + assertEquals(setupMessageCount, playerStates.length); + assertEquals(totalMessageCount, outputMessages.length); + parsedMessage = outputMessages[totalMessageCount - 1]; + assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); + + sendMessage(createMessage('player.setShuffleModeEnabled', { + shuffleModeEnabled: false, + })); + totalMessageCount++; + assertEquals(setupMessageCount, playerStates.length); + assertEquals(totalMessageCount, outputMessages.length); + parsedMessage = outputMessages[totalMessageCount - 1]; + assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); + + sendMessage(createMessage('player.seekTo', { + uuid: 'not_existing', + positionMs: 0, + })); + totalMessageCount++; + assertEquals(setupMessageCount, playerStates.length); + assertEquals(totalMessageCount, outputMessages.length); + parsedMessage = outputMessages[totalMessageCount - 1]; + assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); + }, +}); diff --git a/cast_receiver_app/test/shaka_error_handling_test.js b/cast_receiver_app/test/shaka_error_handling_test.js new file mode 100644 index 0000000000..a7dafd3176 --- /dev/null +++ b/cast_receiver_app/test/shaka_error_handling_test.js @@ -0,0 +1,84 @@ +/** + * 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. + * + * @fileoverview Unit tests for playback methods. + */ + +goog.module('exoplayer.cast.test.shaka'); +goog.setTestOnly(); + +const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); +const Player = goog.require('exoplayer.cast.Player'); +const mocks = goog.require('exoplayer.cast.test.mocks'); +const testSuite = goog.require('goog.testing.testSuite'); +const util = goog.require('exoplayer.cast.test.util'); + +let player; +let shakaFake; + +testSuite({ + setUp() { + mocks.setUp(); + shakaFake = mocks.createShakaFake(); + player = new Player(shakaFake, new ConfigurationFactory()); + }, + + /** Tests Shaka critical error handling on load. */ + async testShakaCriticalError_onload() { + mocks.state().isSilent = true; + mocks.state().setShakaThrowsOnLoad(true); + let playerState; + player.addPlayerListener((state) => { + playerState = state; + }); + player.addQueueItems(0, util.queue.slice(0, 2)); + player.seekToUuid('uuid1', 2000); + player.setPlayWhenReady(true); + // Calling prepare triggers a critical Shaka error. + await player.prepare(); + // Assert player state after error. + assertEquals('IDLE', playerState.playbackState); + assertEquals(mocks.state().shakaError.category, playerState.error.category); + assertEquals(mocks.state().shakaError.code, playerState.error.code); + assertEquals( + 'loading failed for uri: http://example1.com', + playerState.error.message); + assertEquals(999, player.playbackType_); + // Assert player properties are preserved. + assertEquals(2000, player.getCurrentPositionMs()); + assertTrue(player.getPlayWhenReady()); + assertEquals(1, player.getCurrentWindowIndex()); + assertEquals(1, player.windowIndex_); + }, + + /** Tests Shaka critical error handling on unload. */ + async testShakaCriticalError_onunload() { + mocks.state().isSilent = true; + mocks.state().setShakaThrowsOnUnload(true); + let playerState; + player.addPlayerListener((state) => { + playerState = state; + }); + player.addQueueItems(0, util.queue.slice(0, 2)); + player.setPlayWhenReady(true); + assertUndefined(player.videoElement_.src); + // Calling prepare triggers a critical Shaka error. + await player.prepare(); + // Assert player state after caught and ignored error. + await assertEquals('BUFFERING', playerState.playbackState); + assertEquals('http://example.com', player.videoElement_.src); + assertEquals(1, player.playbackType_); + }, +}); diff --git a/cast_receiver_app/test/util.js b/cast_receiver_app/test/util.js new file mode 100644 index 0000000000..22244675b7 --- /dev/null +++ b/cast_receiver_app/test/util.js @@ -0,0 +1,87 @@ +/** + * 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. + * + * @fileoverview Description of this file. + */ + +goog.module('exoplayer.cast.test.util'); +goog.setTestOnly(); + +/** + * The queue of sample media items + * + * @type {!Array} + */ +const queue = [ + { + uuid: 'uuid0', + media: { + uri: 'http://example.com', + }, + mimeType: 'video/*', + }, + { + uuid: 'uuid1', + media: { + uri: 'http://example1.com', + }, + mimeType: 'application/dash+xml', + }, + { + uuid: 'uuid2', + media: { + uri: 'http://example2.com', + }, + mimeType: 'video/*', + }, + { + uuid: 'uuid3', + media: { + uri: 'http://example3.com', + }, + mimeType: 'application/dash+xml', + }, + { + uuid: 'uuid4', + media: { + uri: 'http://example4.com', + }, + mimeType: 'video/*', + }, + { + uuid: 'uuid5', + media: { + uri: 'http://example5.com', + }, + mimeType: 'application/dash+xml', + }, +]; + +/** + * Asserts whether the map of uuids is complete and points to the correct + * indices. + * + * @param {!Object} uuidIndexMap The uuid to index map. + * @param {!Array} queue The media item queue. + */ +const assertUuidIndexMap = (uuidIndexMap, queue) => { + assertEquals(queue.length, Object.entries(uuidIndexMap).length); + queue.forEach((mediaItem, index) => { + assertEquals(uuidIndexMap[mediaItem.uuid], index); + }); +}; + +exports.queue = queue; +exports.assertUuidIndexMap = assertUuidIndexMap; diff --git a/cast_receiver_app/test/validation_test.js b/cast_receiver_app/test/validation_test.js new file mode 100644 index 0000000000..8e58185cfa --- /dev/null +++ b/cast_receiver_app/test/validation_test.js @@ -0,0 +1,266 @@ +/** + * 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. + * + * @fileoverview Unit tests for queue manipulations. + */ + +goog.module('exoplayer.cast.test.validation'); +goog.setTestOnly(); + +const testSuite = goog.require('goog.testing.testSuite'); +const validation = goog.require('exoplayer.cast.validation'); + +/** + * Creates a sample drm media for validation tests. + * + * @return {!Object} A dummy media item with a drm scheme. + */ +const createDrmMedia = function() { + return { + uuid: 'string', + media: { + uri: 'string', + }, + mimeType: 'application/dash+xml', + drmSchemes: [ + { + uuid: 'string', + licenseServer: { + uri: 'string', + requestHeaders: { + 'string': 'string', + }, + }, + }, + ], + }; +}; + +testSuite({ + + /** Tests minimal valid media item. */ + testValidateMediaItem_minimal() { + const mediaItem = { + uuid: 'string', + media: { + uri: 'string', + }, + mimeType: 'application/dash+xml', + }; + assertTrue(validation.validateMediaItem(mediaItem)); + + const uuid = mediaItem.uuid; + delete mediaItem.uuid; + assertFalse(validation.validateMediaItem(mediaItem)); + mediaItem.uuid = uuid; + assertTrue(validation.validateMediaItem(mediaItem)); + + const mimeType = mediaItem.mimeType; + delete mediaItem.mimeType; + assertFalse(validation.validateMediaItem(mediaItem)); + mediaItem.mimeType = mimeType; + assertTrue(validation.validateMediaItem(mediaItem)); + + const media = mediaItem.media; + delete mediaItem.media; + assertFalse(validation.validateMediaItem(mediaItem)); + mediaItem.media = media; + assertTrue(validation.validateMediaItem(mediaItem)); + + const uri = mediaItem.media.uri; + delete mediaItem.media.uri; + assertFalse(validation.validateMediaItem(mediaItem)); + mediaItem.media.uri = uri; + assertTrue(validation.validateMediaItem(mediaItem)); + }, + + /** Tests media item drm property validation. */ + testValidateMediaItem_drmSchemes() { + const mediaItem = createDrmMedia(); + assertTrue(validation.validateMediaItem(mediaItem)); + + const uuid = mediaItem.drmSchemes[0].uuid; + delete mediaItem.drmSchemes[0].uuid; + assertFalse(validation.validateMediaItem(mediaItem)); + mediaItem.drmSchemes[0].uuid = uuid; + assertTrue(validation.validateMediaItem(mediaItem)); + + const licenseServer = mediaItem.drmSchemes[0].licenseServer; + delete mediaItem.drmSchemes[0].licenseServer; + assertFalse(validation.validateMediaItem(mediaItem)); + mediaItem.drmSchemes[0].licenseServer = licenseServer; + assertTrue(validation.validateMediaItem(mediaItem)); + + const uri = mediaItem.drmSchemes[0].licenseServer.uri; + delete mediaItem.drmSchemes[0].licenseServer.uri; + assertFalse(validation.validateMediaItem(mediaItem)); + mediaItem.drmSchemes[0].licenseServer.uri = uri; + assertTrue(validation.validateMediaItem(mediaItem)); + }, + + /** Tests validation of startPositionUs and endPositionUs. */ + testValidateMediaItem_endAndStartPositionUs() { + const mediaItem = createDrmMedia(); + + mediaItem.endPositionUs = 0; + mediaItem.startPositionUs = 120 * 1000; + assertTrue(validation.validateMediaItem(mediaItem)); + + mediaItem.endPositionUs = '0'; + assertFalse(validation.validateMediaItem(mediaItem)); + + mediaItem.endPositionUs = 0; + assertTrue(validation.validateMediaItem(mediaItem)); + + mediaItem.startPositionUs = true; + assertFalse(validation.validateMediaItem(mediaItem)); + }, + + /** Tests validation of the title. */ + testValidateMediaItem_title() { + const mediaItem = createDrmMedia(); + + mediaItem.title = '0'; + assertTrue(validation.validateMediaItem(mediaItem)); + + mediaItem.title = 0; + assertFalse(validation.validateMediaItem(mediaItem)); + }, + + /** Tests validation of the description. */ + testValidateMediaItem_description() { + const mediaItem = createDrmMedia(); + + mediaItem.description = '0'; + assertTrue(validation.validateMediaItem(mediaItem)); + + mediaItem.description = 0; + assertFalse(validation.validateMediaItem(mediaItem)); + }, + + /** Tests validating property of type string. */ + testValidateProperty_string() { + const obj = { + field: 'string', + }; + assertTrue(validation.validateProperty(obj, 'field', 'string')); + assertTrue(validation.validateProperty(obj, 'field', '?string')); + + obj.field = 0; + assertFalse(validation.validateProperty(obj, 'field', 'string')); + assertFalse(validation.validateProperty(obj, 'field', '?string')); + + obj.field = true; + assertFalse(validation.validateProperty(obj, 'field', 'string')); + assertFalse(validation.validateProperty(obj, 'field', '?string')); + + obj.field = {}; + assertFalse(validation.validateProperty(obj, 'field', 'string')); + assertFalse(validation.validateProperty(obj, 'field', '?string')); + + delete obj.field; + assertFalse(validation.validateProperty(obj, 'field', 'string')); + assertTrue(validation.validateProperty(obj, 'field', '?string')); + }, + + /** Tests validating property of type number. */ + testValidateProperty_number() { + const obj = { + field: 0, + }; + assertTrue(validation.validateProperty(obj, 'field', 'number')); + assertTrue(validation.validateProperty(obj, 'field', '?number')); + + obj.field = '0'; + assertFalse(validation.validateProperty(obj, 'field', 'number')); + assertFalse(validation.validateProperty(obj, 'field', '?number')); + + obj.field = true; + assertFalse(validation.validateProperty(obj, 'field', 'number')); + assertFalse(validation.validateProperty(obj, 'field', '?number')); + + obj.field = {}; + assertFalse(validation.validateProperty(obj, 'field', 'number')); + assertFalse(validation.validateProperty(obj, 'field', '?number')); + + delete obj.field; + assertFalse(validation.validateProperty(obj, 'field', 'number')); + assertTrue(validation.validateProperty(obj, 'field', '?number')); + }, + + /** Tests validating property of type boolean. */ + testValidateProperty_boolean() { + const obj = { + field: true, + }; + assertTrue(validation.validateProperty(obj, 'field', 'boolean')); + assertTrue(validation.validateProperty(obj, 'field', '?boolean')); + + obj.field = '0'; + assertFalse(validation.validateProperty(obj, 'field', 'boolean')); + assertFalse(validation.validateProperty(obj, 'field', '?boolean')); + + obj.field = 1000; + assertFalse(validation.validateProperty(obj, 'field', 'boolean')); + assertFalse(validation.validateProperty(obj, 'field', '?boolean')); + + obj.field = [true]; + assertFalse(validation.validateProperty(obj, 'field', 'boolean')); + assertFalse(validation.validateProperty(obj, 'field', '?boolean')); + + delete obj.field; + assertFalse(validation.validateProperty(obj, 'field', 'boolean')); + assertTrue(validation.validateProperty(obj, 'field', '?boolean')); + }, + + /** Tests validating property of type array. */ + testValidateProperty_array() { + const obj = { + field: [], + }; + assertTrue(validation.validateProperty(obj, 'field', 'Array')); + assertTrue(validation.validateProperty(obj, 'field', '?Array')); + + obj.field = '0'; + assertFalse(validation.validateProperty(obj, 'field', 'Array')); + assertFalse(validation.validateProperty(obj, 'field', '?Array')); + + obj.field = 1000; + assertFalse(validation.validateProperty(obj, 'field', 'Array')); + assertFalse(validation.validateProperty(obj, 'field', '?Array')); + + obj.field = true; + assertFalse(validation.validateProperty(obj, 'field', 'Array')); + assertFalse(validation.validateProperty(obj, 'field', '?Array')); + + delete obj.field; + assertFalse(validation.validateProperty(obj, 'field', 'Array')); + assertTrue(validation.validateProperty(obj, 'field', '?Array')); + }, + + /** Tests validating properties of type RepeatMode */ + testValidateProperty_repeatMode() { + const obj = { + off: 'OFF', + one: 'ONE', + all: 'ALL', + invalid: 'invalid', + }; + assertTrue(validation.validateProperty(obj, 'off', 'RepeatMode')); + assertTrue(validation.validateProperty(obj, 'one', 'RepeatMode')); + assertTrue(validation.validateProperty(obj, 'all', 'RepeatMode')); + assertFalse(validation.validateProperty(obj, 'invalid', 'RepeatMode')); + }, +}); diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/ExoCastPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/ExoCastPlayerManager.java new file mode 100644 index 0000000000..e8ad2c1a0d --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/ExoCastPlayerManager.java @@ -0,0 +1,421 @@ +/* + * 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.castdemo; + +import android.content.Context; +import android.net.Uri; +import androidx.annotation.Nullable; +import android.view.KeyEvent; +import android.view.View; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.Player.TimelineChangeReason; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.ext.cast.DefaultCastSessionManager; +import com.google.android.exoplayer2.ext.cast.ExoCastPlayer; +import com.google.android.exoplayer2.ext.cast.MediaItem; +import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; +import com.google.android.gms.cast.framework.CastContext; +import java.util.ArrayList; + +/** Manages players and an internal media queue for the Cast demo app. */ +/* package */ class ExoCastPlayerManager + implements PlayerManager, EventListener, SessionAvailabilityListener { + + private static final String TAG = "ExoCastPlayerManager"; + private static final String USER_AGENT = "ExoCastDemoPlayer"; + private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = + new DefaultHttpDataSourceFactory(USER_AGENT); + + private final PlayerView localPlayerView; + private final PlayerControlView castControlView; + private final SimpleExoPlayer exoPlayer; + private final ExoCastPlayer exoCastPlayer; + private final ArrayList mediaQueue; + private final Listener listener; + private final ConcatenatingMediaSource concatenatingMediaSource; + + private int currentItemIndex; + private Player currentPlayer; + + /** + * Creates a new manager for {@link SimpleExoPlayer} and {@link ExoCastPlayer}. + * + * @param listener A {@link Listener}. + * @param localPlayerView The {@link PlayerView} for local playback. + * @param castControlView The {@link PlayerControlView} to control remote playback. + * @param context A {@link Context}. + * @param castContext The {@link CastContext}. + */ + public ExoCastPlayerManager( + Listener listener, + PlayerView localPlayerView, + PlayerControlView castControlView, + Context context, + CastContext castContext) { + this.listener = listener; + this.localPlayerView = localPlayerView; + this.castControlView = castControlView; + mediaQueue = new ArrayList<>(); + currentItemIndex = C.INDEX_UNSET; + concatenatingMediaSource = new ConcatenatingMediaSource(); + + DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + RenderersFactory renderersFactory = new DefaultRenderersFactory(context); + exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector); + exoPlayer.addListener(this); + localPlayerView.setPlayer(exoPlayer); + + exoCastPlayer = + new ExoCastPlayer( + sessionManagerListener -> + new DefaultCastSessionManager(castContext, sessionManagerListener)); + exoCastPlayer.addListener(this); + exoCastPlayer.setSessionAvailabilityListener(this); + castControlView.setPlayer(exoCastPlayer); + + setCurrentPlayer(exoCastPlayer.isCastSessionAvailable() ? exoCastPlayer : exoPlayer); + } + + // Queue manipulation methods. + + /** + * Plays a specified queue item in the current player. + * + * @param itemIndex The index of the item to play. + */ + @Override + public void selectQueueItem(int itemIndex) { + setCurrentItem(itemIndex, C.TIME_UNSET, true); + } + + /** Returns the index of the currently played item. */ + @Override + public int getCurrentItemIndex() { + return currentItemIndex; + } + + /** + * Appends {@code item} to the media queue. + * + * @param item The {@link MediaItem} to append. + */ + @Override + public void addItem(MediaItem item) { + mediaQueue.add(item); + concatenatingMediaSource.addMediaSource(buildMediaSource(item)); + if (currentPlayer == exoCastPlayer) { + exoCastPlayer.addItemsToQueue(item); + } + } + + /** Returns the size of the media queue. */ + @Override + public int getMediaQueueSize() { + return mediaQueue.size(); + } + + /** + * Returns the item at the given index in the media queue. + * + * @param position The index of the item. + * @return The item at the given index in the media queue. + */ + @Override + public MediaItem getItem(int position) { + return mediaQueue.get(position); + } + + /** + * Removes the item at the given index from the media queue. + * + * @param item The item to remove. + * @return Whether the removal was successful. + */ + @Override + public boolean removeItem(MediaItem item) { + int itemIndex = mediaQueue.indexOf(item); + if (itemIndex == -1) { + // This may happen if another sender app removes items while this sender app is in "swiping + // an item" state. + return false; + } + concatenatingMediaSource.removeMediaSource(itemIndex); + mediaQueue.remove(itemIndex); + if (currentPlayer == exoCastPlayer) { + exoCastPlayer.removeItemFromQueue(itemIndex); + } + if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { + maybeSetCurrentItemAndNotify(C.INDEX_UNSET); + } else if (itemIndex < currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } + return true; + } + + /** + * Moves an item within the queue. + * + * @param item The item to move. This method does nothing if {@code item} is not contained in the + * queue. + * @param toIndex The target index of the item in the queue. If {@code toIndex} exceeds the last + * position in the queue, {@code toIndex} is clamped to match the largest possible value. + * @return True if {@code item} was contained in the queue, and {@code toIndex} was a valid + * position. False otherwise. + */ + @Override + public boolean moveItem(MediaItem item, int toIndex) { + int indexOfItem = mediaQueue.indexOf(item); + if (indexOfItem == -1) { + // This may happen if another sender app removes items while this sender app is in "dragging + // an item" state. + return false; + } + int clampedToIndex = Math.min(toIndex, mediaQueue.size() - 1); + mediaQueue.add(clampedToIndex, mediaQueue.remove(indexOfItem)); + concatenatingMediaSource.moveMediaSource(indexOfItem, clampedToIndex); + if (currentPlayer == exoCastPlayer) { + exoCastPlayer.moveItemInQueue(indexOfItem, clampedToIndex); + } + // Index update. + maybeSetCurrentItemAndNotify(currentPlayer.getCurrentWindowIndex()); + return clampedToIndex == toIndex; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (currentPlayer == exoPlayer) { + return localPlayerView.dispatchKeyEvent(event); + } else /* currentPlayer == exoCastPlayer */ { + return castControlView.dispatchKeyEvent(event); + } + } + + /** Releases the manager and the players that it holds. */ + @Override + public void release() { + currentItemIndex = C.INDEX_UNSET; + mediaQueue.clear(); + concatenatingMediaSource.clear(); + exoCastPlayer.setSessionAvailabilityListener(null); + exoCastPlayer.release(); + localPlayerView.setPlayer(null); + exoPlayer.release(); + } + + // Player.EventListener implementation. + + @Override + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + updateCurrentItemIndex(); + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + updateCurrentItemIndex(); + } + + @Override + public void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { + if (currentPlayer == exoCastPlayer && reason != Player.TIMELINE_CHANGE_REASON_RESET) { + maybeUpdateLocalQueueWithRemoteQueueAndNotify(); + } + updateCurrentItemIndex(); + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + Log.e(TAG, "The player encountered an error.", error); + listener.onPlayerError(); + } + + // CastPlayer.SessionAvailabilityListener implementation. + + @Override + public void onCastSessionAvailable() { + setCurrentPlayer(exoCastPlayer); + } + + @Override + public void onCastSessionUnavailable() { + setCurrentPlayer(exoPlayer); + } + + // Internal methods. + + private void maybeUpdateLocalQueueWithRemoteQueueAndNotify() { + Assertions.checkState(currentPlayer == exoCastPlayer); + boolean mediaQueuesMatch = mediaQueue.size() == exoCastPlayer.getQueueSize(); + for (int i = 0; mediaQueuesMatch && i < mediaQueue.size(); i++) { + mediaQueuesMatch = mediaQueue.get(i).uuid.equals(exoCastPlayer.getQueueItem(i).uuid); + } + if (mediaQueuesMatch) { + // The media queues match. Do nothing. + return; + } + mediaQueue.clear(); + concatenatingMediaSource.clear(); + for (int i = 0; i < exoCastPlayer.getQueueSize(); i++) { + MediaItem item = exoCastPlayer.getQueueItem(i); + mediaQueue.add(item); + concatenatingMediaSource.addMediaSource(buildMediaSource(item)); + } + listener.onQueueContentsExternallyChanged(); + } + + private void updateCurrentItemIndex() { + int playbackState = currentPlayer.getPlaybackState(); + maybeSetCurrentItemAndNotify( + playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED + ? currentPlayer.getCurrentWindowIndex() + : C.INDEX_UNSET); + } + + private void setCurrentPlayer(Player currentPlayer) { + if (this.currentPlayer == currentPlayer) { + return; + } + + // View management. + if (currentPlayer == exoPlayer) { + localPlayerView.setVisibility(View.VISIBLE); + castControlView.hide(); + } else /* currentPlayer == exoCastPlayer */ { + localPlayerView.setVisibility(View.GONE); + castControlView.show(); + } + + // Player state management. + long playbackPositionMs = C.TIME_UNSET; + int windowIndex = C.INDEX_UNSET; + boolean playWhenReady = false; + if (this.currentPlayer != null) { + int playbackState = this.currentPlayer.getPlaybackState(); + if (playbackState != Player.STATE_ENDED) { + playbackPositionMs = this.currentPlayer.getCurrentPosition(); + playWhenReady = this.currentPlayer.getPlayWhenReady(); + windowIndex = this.currentPlayer.getCurrentWindowIndex(); + if (windowIndex != currentItemIndex) { + playbackPositionMs = C.TIME_UNSET; + windowIndex = currentItemIndex; + } + } + this.currentPlayer.stop(true); + } else { + // This is the initial setup. No need to save any state. + } + + this.currentPlayer = currentPlayer; + + // Media queue management. + boolean shouldSeekInNewCurrentPlayer; + if (currentPlayer == exoPlayer) { + exoPlayer.prepare(concatenatingMediaSource); + shouldSeekInNewCurrentPlayer = true; + } else /* currentPlayer == exoCastPlayer */ { + if (exoCastPlayer.getPlaybackState() == Player.STATE_IDLE) { + exoCastPlayer.prepare(); + } + if (mediaQueue.isEmpty()) { + // Casting started with no local queue. We take the receiver app's queue as our own. + maybeUpdateLocalQueueWithRemoteQueueAndNotify(); + shouldSeekInNewCurrentPlayer = false; + } else { + // Casting started when the sender app had no queue. We just load our items into the + // receiver app's queue. If the receiver had no items in its queue, we also seek to wherever + // the sender app was playing. + int currentExoCastPlayerState = exoCastPlayer.getPlaybackState(); + shouldSeekInNewCurrentPlayer = + currentExoCastPlayerState == Player.STATE_IDLE + || currentExoCastPlayerState == Player.STATE_ENDED; + exoCastPlayer.addItemsToQueue(mediaQueue.toArray(new MediaItem[0])); + } + } + + // Playback transition. + if (shouldSeekInNewCurrentPlayer && windowIndex != C.INDEX_UNSET) { + setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); + } else if (getMediaQueueSize() > 0) { + maybeSetCurrentItemAndNotify(currentPlayer.getCurrentWindowIndex()); + } + } + + /** + * Starts playback of the item at the given position. + * + * @param itemIndex The index of the item to play. + * @param positionMs The position at which playback should start. + * @param playWhenReady Whether the player should proceed when ready to do so. + */ + private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { + maybeSetCurrentItemAndNotify(itemIndex); + currentPlayer.seekTo(itemIndex, positionMs); + if (currentPlayer.getPlaybackState() == Player.STATE_IDLE) { + if (currentPlayer == exoCastPlayer) { + exoCastPlayer.prepare(); + } else { + exoPlayer.prepare(concatenatingMediaSource); + } + } + currentPlayer.setPlayWhenReady(playWhenReady); + } + + private void maybeSetCurrentItemAndNotify(int currentItemIndex) { + if (this.currentItemIndex != currentItemIndex) { + int oldIndex = this.currentItemIndex; + this.currentItemIndex = currentItemIndex; + listener.onQueuePositionChanged(oldIndex, currentItemIndex); + } + } + + private static MediaSource buildMediaSource(MediaItem item) { + Uri uri = item.media.uri; + switch (item.mimeType) { + case DemoUtil.MIME_TYPE_SS: + return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + case DemoUtil.MIME_TYPE_DASH: + return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + case DemoUtil.MIME_TYPE_HLS: + return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + case DemoUtil.MIME_TYPE_VIDEO_MP4: + return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + default: + { + throw new IllegalStateException("Unsupported type: " + item.mimeType); + } + } + } +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastSessionManager.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastSessionManager.java new file mode 100644 index 0000000000..7c1f06e8d2 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastSessionManager.java @@ -0,0 +1,86 @@ +/* + * 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.ext.cast; + +/** Handles communication with the receiver app using a cast session. */ +public interface CastSessionManager { + + /** Factory for {@link CastSessionManager} instances. */ + interface Factory { + + /** + * Creates a {@link CastSessionManager} instance with the given listener. + * + * @param listener The listener to notify on receiver app and session state updates. + * @return The created instance. + */ + CastSessionManager create(StateListener listener); + } + + /** + * Extends {@link SessionAvailabilityListener} by adding receiver app state notifications. + * + *

      Receiver app state notifications contain a sequence number that matches the sequence number + * of the last {@link ExoCastMessage} sent (using {@link #send(ExoCastMessage)}) by this session + * manager and processed by the receiver app. Sequence numbers are non-negative numbers. + */ + interface StateListener extends SessionAvailabilityListener { + + /** + * Called when a status update is received from the Cast Receiver app. + * + * @param stateUpdate A {@link ReceiverAppStateUpdate} containing the fields included in the + * message. + */ + void onStateUpdateFromReceiverApp(ReceiverAppStateUpdate stateUpdate); + } + + /** + * Special constant representing an unset sequence number. It is guaranteed to be a negative + * value. + */ + long SEQUENCE_NUMBER_UNSET = Long.MIN_VALUE; + + /** + * Connects the session manager to the cast message bus and starts listening for session + * availability changes. Also announces that this sender app is connected to the message bus. + */ + void start(); + + /** Stops tracking the state of the cast session and closes any existing session. */ + void stopTrackingSession(); + + /** + * Same as {@link #stopTrackingSession()}, but also stops the receiver app if a session is + * currently available. + */ + void stopTrackingSessionAndCasting(); + + /** Whether a cast session is available. */ + boolean isCastSessionAvailable(); + + /** + * Sends an {@link ExoCastMessage} to the receiver app. + * + *

      A sequence number is assigned to every sent message. Message senders may mask the local + * state until a status update from the receiver app (see {@link StateListener}) is received with + * a greater or equal sequence number. + * + * @param message The message to send. + * @return The sequence number assigned to the message. + */ + long send(ExoCastMessage message); +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastSessionManager.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastSessionManager.java new file mode 100644 index 0000000000..c08a9bc352 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastSessionManager.java @@ -0,0 +1,187 @@ +/* + * 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.ext.cast; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; +import com.google.android.gms.cast.Cast; +import com.google.android.gms.cast.CastDevice; +import com.google.android.gms.cast.framework.CastContext; +import com.google.android.gms.cast.framework.CastSession; +import com.google.android.gms.cast.framework.SessionManager; +import com.google.android.gms.cast.framework.SessionManagerListener; +import java.io.IOException; +import org.json.JSONException; + +/** Implements {@link CastSessionManager} by using JSON message passing. */ +public class DefaultCastSessionManager implements CastSessionManager { + + private static final String TAG = "DefaultCastSessionManager"; + private static final String EXOPLAYER_CAST_NAMESPACE = "urn:x-cast:com.google.exoplayer.cast"; + + private final SessionManager sessionManager; + private final CastSessionListener castSessionListener; + private final StateListener stateListener; + private final Cast.MessageReceivedCallback messageReceivedCallback; + + private boolean started; + private long sequenceNumber; + private long expectedInitialStateUpdateSequence; + @Nullable private CastSession currentSession; + + /** + * @param context The Cast context from which the cast session is obtained. + * @param stateListener The listener to notify of state changes. + */ + public DefaultCastSessionManager(CastContext context, StateListener stateListener) { + this.stateListener = stateListener; + sessionManager = context.getSessionManager(); + currentSession = sessionManager.getCurrentCastSession(); + castSessionListener = new CastSessionListener(); + messageReceivedCallback = new CastMessageCallback(); + expectedInitialStateUpdateSequence = SEQUENCE_NUMBER_UNSET; + } + + @Override + public void start() { + started = true; + sessionManager.addSessionManagerListener(castSessionListener, CastSession.class); + currentSession = sessionManager.getCurrentCastSession(); + if (currentSession != null) { + setMessageCallbackOnSession(); + } + } + + @Override + public void stopTrackingSession() { + stop(/* stopCasting= */ false); + } + + @Override + public void stopTrackingSessionAndCasting() { + stop(/* stopCasting= */ true); + } + + @Override + public boolean isCastSessionAvailable() { + return currentSession != null && expectedInitialStateUpdateSequence == SEQUENCE_NUMBER_UNSET; + } + + @Override + public long send(ExoCastMessage message) { + if (currentSession != null) { + currentSession.sendMessage(EXOPLAYER_CAST_NAMESPACE, message.toJsonString(sequenceNumber)); + } else { + Log.w(TAG, "Tried to send a message with no established session. Method: " + message.method); + } + return sequenceNumber++; + } + + private void stop(boolean stopCasting) { + sessionManager.removeSessionManagerListener(castSessionListener, CastSession.class); + if (currentSession != null) { + sessionManager.endCurrentSession(stopCasting); + } + currentSession = null; + started = false; + } + + private void setCastSession(@Nullable CastSession session) { + Assertions.checkState(started); + boolean hadSession = currentSession != null; + currentSession = session; + if (!hadSession && session != null) { + setMessageCallbackOnSession(); + } else if (hadSession && session == null) { + stateListener.onCastSessionUnavailable(); + } + } + + private void setMessageCallbackOnSession() { + try { + Assertions.checkNotNull(currentSession) + .setMessageReceivedCallbacks(EXOPLAYER_CAST_NAMESPACE, messageReceivedCallback); + expectedInitialStateUpdateSequence = send(new ExoCastMessage.OnClientConnected()); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + /** Listens for Cast session state changes. */ + private class CastSessionListener implements SessionManagerListener { + + @Override + public void onSessionStarting(CastSession castSession) {} + + @Override + public void onSessionStarted(CastSession castSession, String sessionId) { + setCastSession(castSession); + } + + @Override + public void onSessionStartFailed(CastSession castSession, int error) {} + + @Override + public void onSessionEnding(CastSession castSession) {} + + @Override + public void onSessionEnded(CastSession castSession, int error) { + setCastSession(null); + } + + @Override + public void onSessionResuming(CastSession castSession, String sessionId) {} + + @Override + public void onSessionResumed(CastSession castSession, boolean wasSuspended) { + setCastSession(castSession); + } + + @Override + public void onSessionResumeFailed(CastSession castSession, int error) {} + + @Override + public void onSessionSuspended(CastSession castSession, int reason) { + setCastSession(null); + } + } + + private class CastMessageCallback implements Cast.MessageReceivedCallback { + + @Override + public void onMessageReceived(CastDevice castDevice, String namespace, String message) { + if (!EXOPLAYER_CAST_NAMESPACE.equals(namespace)) { + // Non-matching namespace. Ignore. + Log.e(TAG, String.format("Unrecognized namespace: '%s'.", namespace)); + return; + } + try { + ReceiverAppStateUpdate receivedUpdate = ReceiverAppStateUpdate.fromJsonMessage(message); + if (expectedInitialStateUpdateSequence == SEQUENCE_NUMBER_UNSET + || receivedUpdate.sequenceNumber >= expectedInitialStateUpdateSequence) { + stateListener.onStateUpdateFromReceiverApp(receivedUpdate); + if (expectedInitialStateUpdateSequence != SEQUENCE_NUMBER_UNSET) { + expectedInitialStateUpdateSequence = SEQUENCE_NUMBER_UNSET; + stateListener.onCastSessionAvailable(); + } + } + } catch (JSONException e) { + Log.e(TAG, "Error while parsing state update from receiver: ", e); + } + } + } +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastConstants.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastConstants.java new file mode 100644 index 0000000000..36173bfc5d --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastConstants.java @@ -0,0 +1,118 @@ +/* + * 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.ext.cast; + +/** Defines constants used by the Cast extension. */ +public final class ExoCastConstants { + + private ExoCastConstants() {} + + public static final int PROTOCOL_VERSION = 0; + + // String representations. + + public static final String STR_STATE_IDLE = "IDLE"; + public static final String STR_STATE_BUFFERING = "BUFFERING"; + public static final String STR_STATE_READY = "READY"; + public static final String STR_STATE_ENDED = "ENDED"; + + public static final String STR_REPEAT_MODE_OFF = "OFF"; + public static final String STR_REPEAT_MODE_ONE = "ONE"; + public static final String STR_REPEAT_MODE_ALL = "ALL"; + + public static final String STR_DISCONTINUITY_REASON_PERIOD_TRANSITION = "PERIOD_TRANSITION"; + public static final String STR_DISCONTINUITY_REASON_SEEK = "SEEK"; + public static final String STR_DISCONTINUITY_REASON_SEEK_ADJUSTMENT = "SEEK_ADJUSTMENT"; + public static final String STR_DISCONTINUITY_REASON_AD_INSERTION = "AD_INSERTION"; + public static final String STR_DISCONTINUITY_REASON_INTERNAL = "INTERNAL"; + + public static final String STR_SELECTION_FLAG_DEFAULT = "DEFAULT"; + public static final String STR_SELECTION_FLAG_FORCED = "FORCED"; + public static final String STR_SELECTION_FLAG_AUTOSELECT = "AUTOSELECT"; + + // Methods. + + public static final String METHOD_BASE = "player."; + + public static final String METHOD_ON_CLIENT_CONNECTED = METHOD_BASE + "onClientConnected"; + public static final String METHOD_ADD_ITEMS = METHOD_BASE + "addItems"; + public static final String METHOD_MOVE_ITEM = METHOD_BASE + "moveItem"; + public static final String METHOD_PREPARE = METHOD_BASE + "prepare"; + public static final String METHOD_REMOVE_ITEMS = METHOD_BASE + "removeItems"; + public static final String METHOD_SET_PLAY_WHEN_READY = METHOD_BASE + "setPlayWhenReady"; + public static final String METHOD_SET_REPEAT_MODE = METHOD_BASE + "setRepeatMode"; + public static final String METHOD_SET_SHUFFLE_MODE_ENABLED = + METHOD_BASE + "setShuffleModeEnabled"; + public static final String METHOD_SEEK_TO = METHOD_BASE + "seekTo"; + public static final String METHOD_SET_PLAYBACK_PARAMETERS = METHOD_BASE + "setPlaybackParameters"; + public static final String METHOD_SET_TRACK_SELECTION_PARAMETERS = + METHOD_BASE + ".setTrackSelectionParameters"; + public static final String METHOD_STOP = METHOD_BASE + "stop"; + + // JSON message keys. + + public static final String KEY_ARGS = "args"; + public static final String KEY_DEFAULT_START_POSITION_US = "defaultStartPositionUs"; + public static final String KEY_DESCRIPTION = "description"; + public static final String KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS = + "disabledTextTrackSelectionFlags"; + public static final String KEY_DISCONTINUITY_REASON = "discontinuityReason"; + public static final String KEY_DRM_SCHEMES = "drmSchemes"; + public static final String KEY_DURATION_US = "durationUs"; + public static final String KEY_END_POSITION_US = "endPositionUs"; + public static final String KEY_ERROR_MESSAGE = "error"; + public static final String KEY_ID = "id"; + public static final String KEY_INDEX = "index"; + public static final String KEY_IS_DYNAMIC = "isDynamic"; + public static final String KEY_IS_LOADING = "isLoading"; + public static final String KEY_IS_SEEKABLE = "isSeekable"; + public static final String KEY_ITEMS = "items"; + public static final String KEY_LICENSE_SERVER = "licenseServer"; + public static final String KEY_MEDIA = "media"; + public static final String KEY_MEDIA_ITEMS_INFO = "mediaItemsInfo"; + public static final String KEY_MEDIA_QUEUE = "mediaQueue"; + public static final String KEY_METHOD = "method"; + public static final String KEY_MIME_TYPE = "mimeType"; + public static final String KEY_PERIOD_ID = "periodId"; + public static final String KEY_PERIODS = "periods"; + public static final String KEY_PITCH = "pitch"; + public static final String KEY_PLAY_WHEN_READY = "playWhenReady"; + public static final String KEY_PLAYBACK_PARAMETERS = "playbackParameters"; + public static final String KEY_PLAYBACK_POSITION = "playbackPosition"; + public static final String KEY_PLAYBACK_STATE = "playbackState"; + public static final String KEY_POSITION_IN_FIRST_PERIOD_US = "positionInFirstPeriodUs"; + public static final String KEY_POSITION_MS = "positionMs"; + public static final String KEY_PREFERRED_AUDIO_LANGUAGE = "preferredAudioLanguage"; + public static final String KEY_PREFERRED_TEXT_LANGUAGE = "preferredTextLanguage"; + public static final String KEY_PROTOCOL_VERSION = "protocolVersion"; + public static final String KEY_REPEAT_MODE = "repeatMode"; + public static final String KEY_REQUEST_HEADERS = "requestHeaders"; + public static final String KEY_RESET = "reset"; + public static final String KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE = + "selectUndeterminedTextLanguage"; + public static final String KEY_SEQUENCE_NUMBER = "sequenceNumber"; + public static final String KEY_SHUFFLE_MODE_ENABLED = "shuffleModeEnabled"; + public static final String KEY_SHUFFLE_ORDER = "shuffleOrder"; + public static final String KEY_SKIP_SILENCE = "skipSilence"; + public static final String KEY_SPEED = "speed"; + public static final String KEY_START_POSITION_US = "startPositionUs"; + public static final String KEY_TITLE = "title"; + public static final String KEY_TRACK_SELECTION_PARAMETERS = "trackSelectionParameters"; + public static final String KEY_URI = "uri"; + public static final String KEY_UUID = "uuid"; + public static final String KEY_UUIDS = "uuids"; + public static final String KEY_WINDOW_DURATION_US = "windowDurationUs"; +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastMessage.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastMessage.java new file mode 100644 index 0000000000..1529e9f5ac --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastMessage.java @@ -0,0 +1,474 @@ +/* + * 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.ext.cast; + +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ARGS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DESCRIPTION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DRM_SCHEMES; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_END_POSITION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_INDEX; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ITEMS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_LICENSE_SERVER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_METHOD; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MIME_TYPE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PROTOCOL_VERSION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REQUEST_HEADERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_RESET; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_START_POSITION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TITLE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_URI; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUIDS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_ADD_ITEMS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_MOVE_ITEM; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_ON_CLIENT_CONNECTED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_PREPARE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_REMOVE_ITEMS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SEEK_TO; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAYBACK_PARAMETERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAY_WHEN_READY; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_REPEAT_MODE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_SHUFFLE_MODE_ENABLED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_TRACK_SELECTION_PARAMETERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_STOP; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.PROTOCOL_VERSION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ONE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_AUTOSELECT; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_DEFAULT; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_FORCED; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ext.cast.MediaItem.UriBundle; +import com.google.android.exoplayer2.source.ShuffleOrder; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +// TODO(Internal b/118432277): Evaluate using a proto for sending to the receiver app. +/** A serializable message for operating a media player. */ +public abstract class ExoCastMessage { + + /** Notifies the receiver app of the connection of a sender app to the message bus. */ + public static final class OnClientConnected extends ExoCastMessage { + + public OnClientConnected() { + super(METHOD_ON_CLIENT_CONNECTED); + } + + @Override + protected JSONObject getArgumentsAsJsonObject() { + // No arguments needed. + return new JSONObject(); + } + } + + /** Transitions the player out of {@link Player#STATE_IDLE}. */ + public static final class Prepare extends ExoCastMessage { + + public Prepare() { + super(METHOD_PREPARE); + } + + @Override + protected JSONObject getArgumentsAsJsonObject() { + // No arguments needed. + return new JSONObject(); + } + } + + /** Transitions the player to {@link Player#STATE_IDLE} and optionally resets its state. */ + public static final class Stop extends ExoCastMessage { + + /** Whether the player state should be reset. */ + public final boolean reset; + + public Stop(boolean reset) { + super(METHOD_STOP); + this.reset = reset; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + return new JSONObject().put(KEY_RESET, reset); + } + } + + /** Adds items to a media player queue. */ + public static final class AddItems extends ExoCastMessage { + + /** + * The index at which the {@link #items} should be inserted. If {@link C#INDEX_UNSET}, the items + * are appended to the queue. + */ + public final int index; + /** The {@link MediaItem items} to add to the media queue. */ + public final List items; + /** + * The shuffle order to use for the media queue that results of adding the items to the queue. + */ + public final ShuffleOrder shuffleOrder; + + /** + * @param index See {@link #index}. + * @param items See {@link #items}. + * @param shuffleOrder See {@link #shuffleOrder}. + */ + public AddItems(int index, List items, ShuffleOrder shuffleOrder) { + super(METHOD_ADD_ITEMS); + this.index = index; + this.items = Collections.unmodifiableList(new ArrayList<>(items)); + this.shuffleOrder = shuffleOrder; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + JSONObject arguments = + new JSONObject() + .put(KEY_ITEMS, getItemsAsJsonArray()) + .put(KEY_SHUFFLE_ORDER, getShuffleOrderAsJson(shuffleOrder)); + maybePutValue(arguments, KEY_INDEX, index, C.INDEX_UNSET); + return arguments; + } + + private JSONArray getItemsAsJsonArray() throws JSONException { + JSONArray result = new JSONArray(); + for (MediaItem item : items) { + result.put(mediaItemAsJsonObject(item)); + } + return result; + } + } + + /** Moves an item in a player media queue. */ + public static final class MoveItem extends ExoCastMessage { + + /** The {@link MediaItem#uuid} of the item to move. */ + public final UUID uuid; + /** The index in the queue to which the item should be moved. */ + public final int index; + /** The shuffle order to use for the media queue that results of moving the item. */ + public ShuffleOrder shuffleOrder; + + /** + * @param uuid See {@link #uuid}. + * @param index See {@link #index}. + * @param shuffleOrder See {@link #shuffleOrder}. + */ + public MoveItem(UUID uuid, int index, ShuffleOrder shuffleOrder) { + super(METHOD_MOVE_ITEM); + this.uuid = uuid; + this.index = index; + this.shuffleOrder = shuffleOrder; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + return new JSONObject() + .put(KEY_UUID, uuid) + .put(KEY_INDEX, index) + .put(KEY_SHUFFLE_ORDER, getShuffleOrderAsJson(shuffleOrder)); + } + } + + /** Removes items from a player queue. */ + public static final class RemoveItems extends ExoCastMessage { + + /** The {@link MediaItem#uuid} of the items to remove from the queue. */ + public final List uuids; + + /** @param uuids See {@link #uuids}. */ + public RemoveItems(List uuids) { + super(METHOD_REMOVE_ITEMS); + this.uuids = Collections.unmodifiableList(new ArrayList<>(uuids)); + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + return new JSONObject().put(KEY_UUIDS, new JSONArray(uuids)); + } + } + + /** See {@link Player#setPlayWhenReady(boolean)}. */ + public static final class SetPlayWhenReady extends ExoCastMessage { + + /** The {@link Player#setPlayWhenReady(boolean) playWhenReady} value to set. */ + public final boolean playWhenReady; + + /** @param playWhenReady See {@link #playWhenReady}. */ + public SetPlayWhenReady(boolean playWhenReady) { + super(METHOD_SET_PLAY_WHEN_READY); + this.playWhenReady = playWhenReady; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + return new JSONObject().put(KEY_PLAY_WHEN_READY, playWhenReady); + } + } + + /** + * Sets the repeat mode of the media player. + * + * @see Player#setRepeatMode(int) + */ + public static final class SetRepeatMode extends ExoCastMessage { + + /** The {@link Player#setRepeatMode(int) repeatMode} to set. */ + @Player.RepeatMode public final int repeatMode; + + /** @param repeatMode See {@link #repeatMode}. */ + public SetRepeatMode(@Player.RepeatMode int repeatMode) { + super(METHOD_SET_REPEAT_MODE); + this.repeatMode = repeatMode; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + return new JSONObject().put(KEY_REPEAT_MODE, repeatModeToString(repeatMode)); + } + + private static String repeatModeToString(@Player.RepeatMode int repeatMode) { + switch (repeatMode) { + case REPEAT_MODE_OFF: + return STR_REPEAT_MODE_OFF; + case REPEAT_MODE_ONE: + return STR_REPEAT_MODE_ONE; + case REPEAT_MODE_ALL: + return STR_REPEAT_MODE_ALL; + default: + throw new AssertionError("Illegal repeat mode: " + repeatMode); + } + } + } + + /** + * Enables and disables shuffle mode in the media player. + * + * @see Player#setShuffleModeEnabled(boolean) + */ + public static final class SetShuffleModeEnabled extends ExoCastMessage { + + /** The {@link Player#setShuffleModeEnabled(boolean) shuffleModeEnabled} value to set. */ + public boolean shuffleModeEnabled; + + /** @param shuffleModeEnabled See {@link #shuffleModeEnabled}. */ + public SetShuffleModeEnabled(boolean shuffleModeEnabled) { + super(METHOD_SET_SHUFFLE_MODE_ENABLED); + this.shuffleModeEnabled = shuffleModeEnabled; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + return new JSONObject().put(KEY_SHUFFLE_MODE_ENABLED, shuffleModeEnabled); + } + } + + /** See {@link Player#seekTo(int, long)}. */ + public static final class SeekTo extends ExoCastMessage { + + /** The {@link MediaItem#uuid} of the item to seek to. */ + public final UUID uuid; + /** + * The seek position in milliseconds in the specified item. If {@link C#TIME_UNSET}, the target + * position is the item's default position. + */ + public final long positionMs; + + /** + * @param uuid See {@link #uuid}. + * @param positionMs See {@link #positionMs}. + */ + public SeekTo(UUID uuid, long positionMs) { + super(METHOD_SEEK_TO); + this.uuid = uuid; + this.positionMs = positionMs; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + JSONObject result = new JSONObject().put(KEY_UUID, uuid); + ExoCastMessage.maybePutValue(result, KEY_POSITION_MS, positionMs, C.TIME_UNSET); + return result; + } + } + + /** See {@link Player#setPlaybackParameters(PlaybackParameters)}. */ + public static final class SetPlaybackParameters extends ExoCastMessage { + + /** The {@link Player#setPlaybackParameters(PlaybackParameters) parameters} to set. */ + public final PlaybackParameters playbackParameters; + + /** @param playbackParameters See {@link #playbackParameters}. */ + public SetPlaybackParameters(PlaybackParameters playbackParameters) { + super(METHOD_SET_PLAYBACK_PARAMETERS); + this.playbackParameters = playbackParameters; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + return new JSONObject() + .put(KEY_SPEED, playbackParameters.speed) + .put(KEY_PITCH, playbackParameters.pitch) + .put(KEY_SKIP_SILENCE, playbackParameters.skipSilence); + } + } + + /** See {@link ExoCastPlayer#setTrackSelectionParameters(TrackSelectionParameters)}. */ + public static final class SetTrackSelectionParameters extends ExoCastMessage { + + /** + * The {@link ExoCastPlayer#setTrackSelectionParameters(TrackSelectionParameters) parameters} to + * set + */ + public final TrackSelectionParameters trackSelectionParameters; + + public SetTrackSelectionParameters(TrackSelectionParameters trackSelectionParameters) { + super(METHOD_SET_TRACK_SELECTION_PARAMETERS); + this.trackSelectionParameters = trackSelectionParameters; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + JSONArray disabledTextSelectionFlagsJson = new JSONArray(); + int disabledSelectionFlags = trackSelectionParameters.disabledTextTrackSelectionFlags; + if ((disabledSelectionFlags & C.SELECTION_FLAG_AUTOSELECT) != 0) { + disabledTextSelectionFlagsJson.put(STR_SELECTION_FLAG_AUTOSELECT); + } + if ((disabledSelectionFlags & C.SELECTION_FLAG_FORCED) != 0) { + disabledTextSelectionFlagsJson.put(STR_SELECTION_FLAG_FORCED); + } + if ((disabledSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0) { + disabledTextSelectionFlagsJson.put(STR_SELECTION_FLAG_DEFAULT); + } + return new JSONObject() + .put(KEY_PREFERRED_AUDIO_LANGUAGE, trackSelectionParameters.preferredAudioLanguage) + .put(KEY_PREFERRED_TEXT_LANGUAGE, trackSelectionParameters.preferredTextLanguage) + .put(KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS, disabledTextSelectionFlagsJson) + .put( + KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE, + trackSelectionParameters.selectUndeterminedTextLanguage); + } + } + + public final String method; + + /** + * Creates a message with the given method. + * + * @param method The method of the message. + */ + protected ExoCastMessage(String method) { + this.method = method; + } + + /** + * Returns a string containing a JSON representation of this message. + * + * @param sequenceNumber The sequence number to associate with this message. + * @return A string containing a JSON representation of this message. + */ + public final String toJsonString(long sequenceNumber) { + try { + JSONObject message = + new JSONObject() + .put(KEY_PROTOCOL_VERSION, PROTOCOL_VERSION) + .put(KEY_METHOD, method) + .put(KEY_SEQUENCE_NUMBER, sequenceNumber) + .put(KEY_ARGS, getArgumentsAsJsonObject()); + return message.toString(); + } catch (JSONException e) { + throw new AssertionError(e); + } + } + + /** Returns a {@link JSONObject} representation of the given item. */ + protected static JSONObject mediaItemAsJsonObject(MediaItem item) throws JSONException { + JSONObject itemAsJson = new JSONObject(); + itemAsJson.put(KEY_UUID, item.uuid); + itemAsJson.put(KEY_TITLE, item.title); + itemAsJson.put(KEY_DESCRIPTION, item.description); + itemAsJson.put(KEY_MEDIA, uriBundleAsJsonObject(item.media)); + // TODO(Internal b/118431961): Add attachment management. + + JSONArray drmSchemesAsJson = new JSONArray(); + for (MediaItem.DrmScheme drmScheme : item.drmSchemes) { + JSONObject drmSchemeAsJson = new JSONObject(); + drmSchemeAsJson.put(KEY_UUID, drmScheme.uuid); + if (drmScheme.licenseServer != null) { + drmSchemeAsJson.put(KEY_LICENSE_SERVER, uriBundleAsJsonObject(drmScheme.licenseServer)); + } + drmSchemesAsJson.put(drmSchemeAsJson); + } + itemAsJson.put(KEY_DRM_SCHEMES, drmSchemesAsJson); + maybePutValue(itemAsJson, KEY_START_POSITION_US, item.startPositionUs, C.TIME_UNSET); + maybePutValue(itemAsJson, KEY_END_POSITION_US, item.endPositionUs, C.TIME_UNSET); + itemAsJson.put(KEY_MIME_TYPE, item.mimeType); + return itemAsJson; + } + + /** Returns a {@link JSONObject JSON object} containing the arguments of the message. */ + protected abstract JSONObject getArgumentsAsJsonObject() throws JSONException; + + /** Returns a JSON representation of the given {@link UriBundle}. */ + protected static JSONObject uriBundleAsJsonObject(UriBundle uriBundle) throws JSONException { + return new JSONObject() + .put(KEY_URI, uriBundle.uri) + .put(KEY_REQUEST_HEADERS, new JSONObject(uriBundle.requestHeaders)); + } + + private static JSONArray getShuffleOrderAsJson(ShuffleOrder shuffleOrder) { + JSONArray shuffleOrderJson = new JSONArray(); + int index = shuffleOrder.getFirstIndex(); + while (index != C.INDEX_UNSET) { + shuffleOrderJson.put(index); + index = shuffleOrder.getNextIndex(index); + } + return shuffleOrderJson; + } + + private static void maybePutValue(JSONObject target, String key, long value, long unsetValue) + throws JSONException { + if (value != unsetValue) { + target.put(key, value); + } + } +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastOptionsProvider.java new file mode 100644 index 0000000000..56b5d3cc8c --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastOptionsProvider.java @@ -0,0 +1,40 @@ +/* + * 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.ext.cast; + +import android.content.Context; +import androidx.annotation.Nullable; +import com.google.android.gms.cast.framework.CastOptions; +import com.google.android.gms.cast.framework.OptionsProvider; +import com.google.android.gms.cast.framework.SessionProvider; +import java.util.List; + +/** Cast options provider to target ExoPlayer's custom receiver app. */ +public final class ExoCastOptionsProvider implements OptionsProvider { + + public static final String RECEIVER_ID = "365DCC88"; + + @Override + public CastOptions getCastOptions(Context context) { + return new CastOptions.Builder().setReceiverApplicationId(RECEIVER_ID).build(); + } + + @Override + @Nullable + public List getAdditionalSessionProviders(Context context) { + return null; + } +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayer.java new file mode 100644 index 0000000000..e24970ba0d --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayer.java @@ -0,0 +1,958 @@ +/* + * 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.ext.cast; + +import android.os.Looper; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.BasePlayer; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.IllegalSeekPositionException; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.ext.cast.ExoCastMessage.AddItems; +import com.google.android.exoplayer2.ext.cast.ExoCastMessage.MoveItem; +import com.google.android.exoplayer2.ext.cast.ExoCastMessage.RemoveItems; +import com.google.android.exoplayer2.ext.cast.ExoCastMessage.SetRepeatMode; +import com.google.android.exoplayer2.ext.cast.ExoCastMessage.SetShuffleModeEnabled; +import com.google.android.exoplayer2.ext.cast.ExoCastMessage.SetTrackSelectionParameters; +import com.google.android.exoplayer2.ext.cast.ExoCastTimeline.PeriodUid; +import com.google.android.exoplayer2.source.ShuffleOrder; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; +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.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** + * Plays media in a Cast receiver app that implements the ExoCast message protocol. + * + *

      The ExoCast communication protocol consists in exchanging serialized {@link ExoCastMessage + * ExoCastMessages} and {@link ReceiverAppStateUpdate receiver app state updates}. + * + *

      All methods in this class must be invoked on the main thread. Operations that change the state + * of the receiver app are masked locally as if their effect was immediate in the receiver app. + * + *

      Methods that change the state of the player must only be invoked when a session is available, + * according to {@link CastSessionManager#isCastSessionAvailable()}. + */ +public final class ExoCastPlayer extends BasePlayer { + + private static final String TAG = "ExoCastPlayer"; + + private static final int RENDERER_COUNT = 4; + private static final int RENDERER_INDEX_VIDEO = 0; + private static final int RENDERER_INDEX_AUDIO = 1; + private static final int RENDERER_INDEX_TEXT = 2; + private static final int RENDERER_INDEX_METADATA = 3; + + private final Clock clock; + private final CastSessionManager castSessionManager; + private final CopyOnWriteArrayList listeners; + private final ArrayList notificationsBatch; + private final ArrayDeque ongoingNotificationsTasks; + private final Timeline.Period scratchPeriod; + @Nullable private SessionAvailabilityListener sessionAvailabilityListener; + + // Player state. + + private final List mediaItems; + private final StateHolder currentTimeline; + private ShuffleOrder currentShuffleOrder; + + private final StateHolder playbackState; + private final StateHolder playWhenReady; + private final StateHolder repeatMode; + private final StateHolder shuffleModeEnabled; + private final StateHolder isLoading; + private final StateHolder playbackParameters; + private final StateHolder trackselectionParameters; + private final StateHolder currentTrackGroups; + private final StateHolder currentTrackSelections; + private final StateHolder<@NullableType Object> currentManifest; + private final StateHolder<@NullableType PeriodUid> currentPeriodUid; + private final StateHolder playbackPositionMs; + private final HashMap currentMediaItemInfoMap; + private long lastPlaybackPositionChangeTimeMs; + @Nullable private ExoPlaybackException playbackError; + + /** + * Creates an instance using the system clock for calculating time deltas. + * + * @param castSessionManagerFactory Factory to create the {@link CastSessionManager}. + */ + public ExoCastPlayer(CastSessionManager.Factory castSessionManagerFactory) { + this(castSessionManagerFactory, Clock.DEFAULT); + } + + /** + * Creates an instance using a custom {@link Clock} implementation. + * + * @param castSessionManagerFactory Factory to create the {@link CastSessionManager}. + * @param clock The clock to use for time delta calculations. + */ + public ExoCastPlayer(CastSessionManager.Factory castSessionManagerFactory, Clock clock) { + this.clock = clock; + castSessionManager = castSessionManagerFactory.create(new SessionManagerStateListener()); + listeners = new CopyOnWriteArrayList<>(); + notificationsBatch = new ArrayList<>(); + ongoingNotificationsTasks = new ArrayDeque<>(); + scratchPeriod = new Timeline.Period(); + mediaItems = new ArrayList<>(); + currentShuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ mediaItems.size()); + playbackState = new StateHolder<>(STATE_IDLE); + playWhenReady = new StateHolder<>(false); + repeatMode = new StateHolder<>(REPEAT_MODE_OFF); + shuffleModeEnabled = new StateHolder<>(false); + isLoading = new StateHolder<>(false); + playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT); + trackselectionParameters = new StateHolder<>(TrackSelectionParameters.DEFAULT); + currentTrackGroups = new StateHolder<>(TrackGroupArray.EMPTY); + currentTrackSelections = new StateHolder<>(new TrackSelectionArray(null, null, null, null)); + currentManifest = new StateHolder<>(null); + currentTimeline = new StateHolder<>(ExoCastTimeline.EMPTY); + playbackPositionMs = new StateHolder<>(0L); + currentPeriodUid = new StateHolder<>(null); + currentMediaItemInfoMap = new HashMap<>(); + castSessionManager.start(); + } + + /** Returns whether a Cast session is available. */ + public boolean isCastSessionAvailable() { + return castSessionManager.isCastSessionAvailable(); + } + + /** + * Sets a listener for updates on the Cast session availability. + * + * @param listener The {@link SessionAvailabilityListener}. + */ + public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) { + sessionAvailabilityListener = listener; + } + + /** + * Prepares the player for playback. + * + *

      Sends a preparation message to the receiver. If the player is in {@link #STATE_IDLE}, + * updates the timeline with the media queue contents. + */ + public void prepare() { + long sequence = castSessionManager.send(new ExoCastMessage.Prepare()); + if (playbackState.value == STATE_IDLE) { + playbackState.sequence = sequence; + setPlaybackStateInternal(mediaItems.isEmpty() ? STATE_ENDED : STATE_BUFFERING); + if (!currentTimeline.value.representsMediaQueue( + mediaItems, currentMediaItemInfoMap, currentShuffleOrder)) { + updateTimelineInternal(TIMELINE_CHANGE_REASON_PREPARED); + } + } + flushNotifications(); + } + + /** + * Returns the item at the given index. + * + * @param index The index of the item to retrieve. + * @return The item at the given index. + */ + public MediaItem getQueueItem(int index) { + return mediaItems.get(index); + } + + /** + * Equivalent to {@link #addItemsToQueue(int, MediaItem...) addItemsToQueue(C.INDEX_UNSET, + * items)}. + */ + public void addItemsToQueue(MediaItem... items) { + addItemsToQueue(C.INDEX_UNSET, items); + } + + /** + * Adds the given sequence of items to the queue at the given position, so that the first of + * {@code items} is placed at the given index. + * + *

      This method discards {@code items} with a uuid that already appears in the media queue. This + * method does nothing if {@code items} contains no new items. + * + * @param optionalIndex The index at which {@code items} will be inserted. If {@link + * C#INDEX_UNSET} is passed, the items are appended to the media queue. + * @param items The sequence of items to append. {@code items} must not contain items with + * matching uuids. + * @throws IllegalArgumentException If two or more elements in {@code items} contain matching + * uuids. + */ + public void addItemsToQueue(int optionalIndex, MediaItem... items) { + // Filter out items whose uuid already appears in the queue. + ArrayList itemsToAdd = new ArrayList<>(); + HashSet addedUuids = new HashSet<>(); + for (MediaItem item : items) { + Assertions.checkArgument( + addedUuids.add(item.uuid), "Added items must contain distinct uuids"); + if (playbackState.value == STATE_IDLE + || currentTimeline.value.getWindowIndexFromUuid(item.uuid) == C.INDEX_UNSET) { + // Prevent adding items that exist in the timeline. If the player is not yet prepared, + // ignore this check, since the timeline may not reflect the current media queue. + // Preparation will filter any duplicates. + itemsToAdd.add(item); + } + } + if (itemsToAdd.isEmpty()) { + return; + } + + int normalizedIndex; + if (optionalIndex != C.INDEX_UNSET) { + normalizedIndex = optionalIndex; + mediaItems.addAll(optionalIndex, itemsToAdd); + } else { + normalizedIndex = mediaItems.size(); + mediaItems.addAll(itemsToAdd); + } + currentShuffleOrder = currentShuffleOrder.cloneAndInsert(normalizedIndex, itemsToAdd.size()); + long sequence = + castSessionManager.send(new AddItems(optionalIndex, itemsToAdd, currentShuffleOrder)); + if (playbackState.value != STATE_IDLE) { + currentTimeline.sequence = sequence; + updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC); + } + flushNotifications(); + } + + /** + * Moves an existing item within the queue. + * + *

      Calling this method is equivalent to removing the item at position {@code indexFrom} and + * immediately inserting it at position {@code indexTo}. If the moved item is being played at the + * moment of the invocation, playback will stick with the moved item. + * + * @param index The index of the item to move. + * @param newIndex The index at which the item will be placed after this operation. + */ + public void moveItemInQueue(int index, int newIndex) { + MediaItem movedItem = mediaItems.remove(index); + mediaItems.add(newIndex, movedItem); + currentShuffleOrder = + currentShuffleOrder + .cloneAndRemove(index, index + 1) + .cloneAndInsert(newIndex, /* insertionCount= */ 1); + long sequence = + castSessionManager.send(new MoveItem(movedItem.uuid, newIndex, currentShuffleOrder)); + if (playbackState.value != STATE_IDLE) { + currentTimeline.sequence = sequence; + updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC); + } + flushNotifications(); + } + + /** + * Removes an item from the queue. + * + * @param index The index of the item to remove from the queue. + */ + public void removeItemFromQueue(int index) { + removeRangeFromQueue(index, index + 1); + } + + /** + * Removes a range of items from the queue. + * + *

      If the currently-playing item is removed, the playback position moves to the item following + * the removed range. If no item follows the removed range, the position is set to the last item + * in the queue and the player state transitions to {@link #STATE_ENDED}. Does nothing if an empty + * range ({@code from == exclusiveTo}) is passed. + * + * @param indexFrom The inclusive index at which the range to remove starts. + * @param indexExclusiveTo The exclusive index at which the range to remove ends. + */ + public void removeRangeFromQueue(int indexFrom, int indexExclusiveTo) { + UUID[] uuidsToRemove = new UUID[indexExclusiveTo - indexFrom]; + for (int i = 0; i < uuidsToRemove.length; i++) { + uuidsToRemove[i] = mediaItems.get(i + indexFrom).uuid; + } + + int windowIndexBeforeRemoval = getCurrentWindowIndex(); + boolean currentItemWasRemoved = + windowIndexBeforeRemoval >= indexFrom && windowIndexBeforeRemoval < indexExclusiveTo; + boolean shouldTransitionToEnded = + currentItemWasRemoved && indexExclusiveTo == mediaItems.size(); + + Util.removeRange(mediaItems, indexFrom, indexExclusiveTo); + long sequence = castSessionManager.send(new RemoveItems(Arrays.asList(uuidsToRemove))); + currentShuffleOrder = currentShuffleOrder.cloneAndRemove(indexFrom, indexExclusiveTo); + + if (playbackState.value != STATE_IDLE) { + currentTimeline.sequence = sequence; + updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC); + if (currentItemWasRemoved) { + int newWindowIndex = Math.max(0, indexFrom - (shouldTransitionToEnded ? 1 : 0)); + PeriodUid periodUid = + currentTimeline.value.isEmpty() + ? null + : (PeriodUid) + currentTimeline.value.getPeriodPosition( + window, + scratchPeriod, + newWindowIndex, + /* windowPositionUs= */ C.TIME_UNSET) + .first; + currentPeriodUid.sequence = sequence; + playbackPositionMs.sequence = sequence; + setPlaybackPositionInternal( + periodUid, + /* positionMs= */ C.TIME_UNSET, + /* discontinuityReason= */ DISCONTINUITY_REASON_SEEK); + } + playbackState.sequence = sequence; + setPlaybackStateInternal(shouldTransitionToEnded ? STATE_ENDED : STATE_BUFFERING); + } + flushNotifications(); + } + + /** Removes all items in the queue. */ + public void clearQueue() { + removeRangeFromQueue(0, getQueueSize()); + } + + /** Returns the number of items in this queue. */ + public int getQueueSize() { + return mediaItems.size(); + } + + // Track selection. + + /** + * Provides a set of constrains for the receiver app to execute track selection. + * + *

      {@link TrackSelectionParameters} passed to this method may be {@link + * TrackSelectionParameters#buildUpon() built upon} by this player as a result of a remote + * operation, which means {@link TrackSelectionParameters} obtained from {@link + * #getTrackSelectionParameters()} may have field differences with {@code parameters} passed to + * this method. However, only fields modified remotely will present differences. Other fields will + * remain unchanged. + */ + public void setTrackSelectionParameters(TrackSelectionParameters trackselectionParameters) { + this.trackselectionParameters.value = trackselectionParameters; + this.trackselectionParameters.sequence = + castSessionManager.send(new SetTrackSelectionParameters(trackselectionParameters)); + } + + /** + * Retrieves the current {@link TrackSelectionParameters}. See {@link + * #setTrackSelectionParameters(TrackSelectionParameters)}. + */ + public TrackSelectionParameters getTrackSelectionParameters() { + return trackselectionParameters.value; + } + + // Player Implementation. + + @Override + @Nullable + public AudioComponent getAudioComponent() { + // TODO: Implement volume controls using the audio component. + return null; + } + + @Override + @Nullable + public VideoComponent getVideoComponent() { + return null; + } + + @Override + @Nullable + public TextComponent getTextComponent() { + return null; + } + + @Override + @Nullable + public MetadataComponent getMetadataComponent() { + return null; + } + + @Override + public Looper getApplicationLooper() { + return Looper.getMainLooper(); + } + + @Override + public void addListener(EventListener listener) { + listeners.addIfAbsent(new ListenerHolder(listener)); + } + + @Override + public void removeListener(EventListener listener) { + for (ListenerHolder listenerHolder : listeners) { + if (listenerHolder.listener.equals(listener)) { + listenerHolder.release(); + listeners.remove(listenerHolder); + } + } + } + + @Override + @Player.State + public int getPlaybackState() { + return playbackState.value; + } + + @Nullable + @Override + public ExoPlaybackException getPlaybackError() { + return playbackError; + } + + @Override + public void setPlayWhenReady(boolean playWhenReady) { + this.playWhenReady.sequence = + castSessionManager.send(new ExoCastMessage.SetPlayWhenReady(playWhenReady)); + // Take a snapshot of the playback position before pausing to ensure future calculations are + // correct. + setPlaybackPositionInternal( + currentPeriodUid.value, getCurrentPosition(), /* discontinuityReason= */ null); + setPlayWhenReadyInternal(playWhenReady); + flushNotifications(); + } + + @Override + public boolean getPlayWhenReady() { + return playWhenReady.value; + } + + @Override + public void setRepeatMode(@RepeatMode int repeatMode) { + this.repeatMode.sequence = castSessionManager.send(new SetRepeatMode(repeatMode)); + setRepeatModeInternal(repeatMode); + flushNotifications(); + } + + @Override + @RepeatMode + public int getRepeatMode() { + return repeatMode.value; + } + + @Override + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + this.shuffleModeEnabled.sequence = + castSessionManager.send(new SetShuffleModeEnabled(shuffleModeEnabled)); + setShuffleModeEnabledInternal(shuffleModeEnabled); + flushNotifications(); + } + + @Override + public boolean getShuffleModeEnabled() { + return shuffleModeEnabled.value; + } + + @Override + public boolean isLoading() { + return isLoading.value; + } + + @Override + public void seekTo(int windowIndex, long positionMs) { + if (mediaItems.isEmpty()) { + // TODO: Handle seeking in empty timeline. + setPlaybackPositionInternal(/* periodUid= */ null, 0, DISCONTINUITY_REASON_SEEK); + return; + } else if (windowIndex >= mediaItems.size()) { + throw new IllegalSeekPositionException(currentTimeline.value, windowIndex, positionMs); + } + long sequence = + castSessionManager.send( + new ExoCastMessage.SeekTo(mediaItems.get(windowIndex).uuid, positionMs)); + + currentPeriodUid.sequence = sequence; + playbackPositionMs.sequence = sequence; + + PeriodUid periodUid = + (PeriodUid) + currentTimeline.value.getPeriodPosition( + window, scratchPeriod, windowIndex, C.msToUs(positionMs)) + .first; + setPlaybackPositionInternal(periodUid, positionMs, DISCONTINUITY_REASON_SEEK); + if (playbackState.value != STATE_IDLE) { + playbackState.sequence = sequence; + setPlaybackStateInternal(STATE_BUFFERING); + } + flushNotifications(); + } + + @Override + public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { + playbackParameters = + playbackParameters != null ? playbackParameters : PlaybackParameters.DEFAULT; + this.playbackParameters.value = playbackParameters; + this.playbackParameters.sequence = + castSessionManager.send(new ExoCastMessage.SetPlaybackParameters(playbackParameters)); + this.playbackParameters.value = playbackParameters; + // Note: This method, unlike others, does not immediately notify the change. See the Player + // interface for more information. + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return playbackParameters.value; + } + + @Override + public void stop(boolean reset) { + long sequence = castSessionManager.send(new ExoCastMessage.Stop(reset)); + playbackState.sequence = sequence; + setPlaybackStateInternal(STATE_IDLE); + if (reset) { + currentTimeline.sequence = sequence; + mediaItems.clear(); + currentShuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length =*/ 0); + setPlaybackPositionInternal( + /* periodUid= */ null, /* positionMs= */ 0, DISCONTINUITY_REASON_INTERNAL); + updateTimelineInternal(TIMELINE_CHANGE_REASON_RESET); + } + flushNotifications(); + } + + @Override + public void release() { + setSessionAvailabilityListener(null); + castSessionManager.stopTrackingSession(); + flushNotifications(); + } + + @Override + public int getRendererCount() { + return RENDERER_COUNT; + } + + @Override + public int getRendererType(int index) { + switch (index) { + case RENDERER_INDEX_VIDEO: + return C.TRACK_TYPE_VIDEO; + case RENDERER_INDEX_AUDIO: + return C.TRACK_TYPE_AUDIO; + case RENDERER_INDEX_TEXT: + return C.TRACK_TYPE_TEXT; + case RENDERER_INDEX_METADATA: + return C.TRACK_TYPE_METADATA; + default: + throw new IndexOutOfBoundsException(); + } + } + + @Override + public TrackGroupArray getCurrentTrackGroups() { + // TODO (Internal b/62080507): Implement using track information from currentMediaItemInfoMap. + return currentTrackGroups.value; + } + + @Override + public TrackSelectionArray getCurrentTrackSelections() { + // TODO (Internal b/62080507): Implement using track information from currentMediaItemInfoMap. + return currentTrackSelections.value; + } + + @Override + @Nullable + public Object getCurrentManifest() { + // TODO (Internal b/62080507): Implement using track information from currentMediaItemInfoMap. + return currentManifest.value; + } + + @Override + public Timeline getCurrentTimeline() { + return currentTimeline.value; + } + + @Override + public int getCurrentPeriodIndex() { + int periodIndex = + currentPeriodUid.value == null + ? C.INDEX_UNSET + : currentTimeline.value.getIndexOfPeriod(currentPeriodUid.value); + return periodIndex != C.INDEX_UNSET ? periodIndex : 0; + } + + @Override + public int getCurrentWindowIndex() { + int windowIndex = + currentPeriodUid.value == null + ? C.INDEX_UNSET + : currentTimeline.value.getWindowIndexContainingPeriod(currentPeriodUid.value); + return windowIndex != C.INDEX_UNSET ? windowIndex : 0; + } + + @Override + public long getDuration() { + return getContentDuration(); + } + + @Override + public long getCurrentPosition() { + return playbackPositionMs.value + + (getPlaybackState() == STATE_READY && getPlayWhenReady() + ? projectPlaybackTimeElapsedMs() + : 0L); + } + + @Override + public long getBufferedPosition() { + return getCurrentPosition(); + } + + @Override + public long getTotalBufferedDuration() { + return 0; + } + + @Override + public boolean isPlayingAd() { + // TODO (Internal b/119293631): Add support for ads. + return false; + } + + @Override + public int getCurrentAdGroupIndex() { + return C.INDEX_UNSET; + } + + @Override + public int getCurrentAdIndexInAdGroup() { + return C.INDEX_UNSET; + } + + @Override + public long getContentPosition() { + return getCurrentPosition(); + } + + @Override + public long getContentBufferedPosition() { + return getCurrentPosition(); + } + + // Local state modifications. + + private void setPlayWhenReadyInternal(boolean playWhenReady) { + if (this.playWhenReady.value != playWhenReady) { + this.playWhenReady.value = playWhenReady; + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onPlayerStateChanged(playWhenReady, playbackState.value))); + } + } + + private void setPlaybackStateInternal(int playbackState) { + if (this.playbackState.value != playbackState) { + if (this.playbackState.value == STATE_IDLE) { + // We are transitioning out of STATE_IDLE. We clear any errors. + setPlaybackErrorInternal(null); + } + this.playbackState.value = playbackState; + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onPlayerStateChanged(playWhenReady.value, playbackState))); + } + } + + private void setRepeatModeInternal(int repeatMode) { + if (this.repeatMode.value != repeatMode) { + this.repeatMode.value = repeatMode; + notificationsBatch.add( + new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(repeatMode))); + } + } + + private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled) { + if (this.shuffleModeEnabled.value != shuffleModeEnabled) { + this.shuffleModeEnabled.value = shuffleModeEnabled; + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled))); + } + } + + private void setIsLoadingInternal(boolean isLoading) { + if (this.isLoading.value != isLoading) { + this.isLoading.value = isLoading; + notificationsBatch.add( + new ListenerNotificationTask(listener -> listener.onLoadingChanged(isLoading))); + } + } + + private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) { + if (!this.playbackParameters.value.equals(playbackParameters)) { + this.playbackParameters.value = playbackParameters; + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onPlaybackParametersChanged(playbackParameters))); + } + } + + private void setPlaybackErrorInternal(@Nullable String errorMessage) { + if (errorMessage != null) { + playbackError = ExoPlaybackException.createForRemote(errorMessage); + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onPlayerError(Assertions.checkNotNull(playbackError)))); + } else { + playbackError = null; + } + } + + private void setPlaybackPositionInternal( + @Nullable PeriodUid periodUid, long positionMs, @Nullable Integer discontinuityReason) { + currentPeriodUid.value = periodUid; + if (periodUid == null) { + positionMs = 0L; + } else if (positionMs == C.TIME_UNSET) { + int windowIndex = currentTimeline.value.getWindowIndexContainingPeriod(periodUid); + if (windowIndex == C.INDEX_UNSET) { + positionMs = 0; + } else { + positionMs = + C.usToMs( + currentTimeline.value.getWindow(windowIndex, window, /* setTag= */ false) + .defaultPositionUs); + } + } + playbackPositionMs.value = positionMs; + lastPlaybackPositionChangeTimeMs = clock.elapsedRealtime(); + if (discontinuityReason != null) { + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onPositionDiscontinuity(discontinuityReason))); + } + } + + // Internal methods. + + private void updateTimelineInternal(@TimelineChangeReason int changeReason) { + currentTimeline.value = + ExoCastTimeline.createTimelineFor(mediaItems, currentMediaItemInfoMap, currentShuffleOrder); + removeStaleMediaItemInfo(); + notificationsBatch.add( + new ListenerNotificationTask( + listener -> + listener.onTimelineChanged( + currentTimeline.value, /* manifest= */ null, changeReason))); + } + + private long projectPlaybackTimeElapsedMs() { + return (long) + ((clock.elapsedRealtime() - lastPlaybackPositionChangeTimeMs) + * playbackParameters.value.speed); + } + + private void flushNotifications() { + boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty(); + ongoingNotificationsTasks.addAll(notificationsBatch); + notificationsBatch.clear(); + if (recursiveNotification) { + // This will be handled once the current notification task is finished. + return; + } + while (!ongoingNotificationsTasks.isEmpty()) { + ongoingNotificationsTasks.peekFirst().execute(); + ongoingNotificationsTasks.removeFirst(); + } + } + + /** + * Updates the current media item information by including any extra entries received from the + * receiver app. + * + * @param mediaItemsInformation A map of media item information received from the receiver app. + */ + private void updateMediaItemsInfo(Map mediaItemsInformation) { + for (Map.Entry entry : mediaItemsInformation.entrySet()) { + MediaItemInfo currentInfoForEntry = currentMediaItemInfoMap.get(entry.getKey()); + boolean shouldPutEntry = + currentInfoForEntry == null || !currentInfoForEntry.equals(entry.getValue()); + if (shouldPutEntry) { + currentMediaItemInfoMap.put(entry.getKey(), entry.getValue()); + } + } + } + + /** + * Removes stale media info entries. An entry is considered stale when the corresponding media + * item is not present in the current media queue. + */ + private void removeStaleMediaItemInfo() { + for (Iterator iterator = currentMediaItemInfoMap.keySet().iterator(); + iterator.hasNext(); ) { + UUID uuid = iterator.next(); + if (currentTimeline.value.getWindowIndexFromUuid(uuid) == C.INDEX_UNSET) { + iterator.remove(); + } + } + } + + // Internal classes. + + private class SessionManagerStateListener implements CastSessionManager.StateListener { + + @Override + public void onCastSessionAvailable() { + if (sessionAvailabilityListener != null) { + sessionAvailabilityListener.onCastSessionAvailable(); + } + } + + @Override + public void onCastSessionUnavailable() { + if (sessionAvailabilityListener != null) { + sessionAvailabilityListener.onCastSessionUnavailable(); + } + } + + @Override + public void onStateUpdateFromReceiverApp(ReceiverAppStateUpdate stateUpdate) { + long sequence = stateUpdate.sequenceNumber; + + if (stateUpdate.errorMessage != null) { + setPlaybackErrorInternal(stateUpdate.errorMessage); + } + + if (sequence >= playbackState.sequence && stateUpdate.playbackState != null) { + setPlaybackStateInternal(stateUpdate.playbackState); + } + + if (sequence >= currentTimeline.sequence) { + if (stateUpdate.items != null) { + mediaItems.clear(); + mediaItems.addAll(stateUpdate.items); + } + + currentShuffleOrder = + stateUpdate.shuffleOrder != null + ? new ShuffleOrder.DefaultShuffleOrder( + Util.toArray(stateUpdate.shuffleOrder), clock.elapsedRealtime()) + : currentShuffleOrder; + updateMediaItemsInfo(stateUpdate.mediaItemsInformation); + + if (playbackState.value != STATE_IDLE + && !currentTimeline.value.representsMediaQueue( + mediaItems, currentMediaItemInfoMap, currentShuffleOrder)) { + updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC); + } + } + + if (sequence >= currentPeriodUid.sequence + && stateUpdate.currentPlayingItemUuid != null + && stateUpdate.currentPlaybackPositionMs != null) { + PeriodUid periodUid; + if (stateUpdate.currentPlayingPeriodId == null) { + int windowIndex = + currentTimeline.value.getWindowIndexFromUuid(stateUpdate.currentPlayingItemUuid); + periodUid = + (PeriodUid) + currentTimeline.value.getPeriodPosition( + window, + scratchPeriod, + windowIndex, + C.msToUs(stateUpdate.currentPlaybackPositionMs)) + .first; + } else { + periodUid = + ExoCastTimeline.createPeriodUid( + stateUpdate.currentPlayingItemUuid, stateUpdate.currentPlayingPeriodId); + } + setPlaybackPositionInternal( + periodUid, stateUpdate.currentPlaybackPositionMs, stateUpdate.discontinuityReason); + } + + if (sequence >= isLoading.sequence && stateUpdate.isLoading != null) { + setIsLoadingInternal(stateUpdate.isLoading); + } + + if (sequence >= playWhenReady.sequence && stateUpdate.playWhenReady != null) { + setPlayWhenReadyInternal(stateUpdate.playWhenReady); + } + + if (sequence >= shuffleModeEnabled.sequence && stateUpdate.shuffleModeEnabled != null) { + setShuffleModeEnabledInternal(stateUpdate.shuffleModeEnabled); + } + + if (sequence >= repeatMode.sequence && stateUpdate.repeatMode != null) { + setRepeatModeInternal(stateUpdate.repeatMode); + } + + if (sequence >= playbackParameters.sequence && stateUpdate.playbackParameters != null) { + setPlaybackParametersInternal(stateUpdate.playbackParameters); + } + + TrackSelectionParameters parameters = stateUpdate.trackSelectionParameters; + if (sequence >= trackselectionParameters.sequence && parameters != null) { + trackselectionParameters.value = + trackselectionParameters + .value + .buildUpon() + .setDisabledTextTrackSelectionFlags(parameters.disabledTextTrackSelectionFlags) + .setPreferredAudioLanguage(parameters.preferredAudioLanguage) + .setPreferredTextLanguage(parameters.preferredTextLanguage) + .setSelectUndeterminedTextLanguage(parameters.selectUndeterminedTextLanguage) + .build(); + } + + flushNotifications(); + } + } + + private static final class StateHolder { + + public T value; + public long sequence; + + public StateHolder(T initialValue) { + value = initialValue; + sequence = CastSessionManager.SEQUENCE_NUMBER_UNSET; + } + } + + private final class ListenerNotificationTask { + + private final Iterator listenersSnapshot; + private final ListenerInvocation listenerInvocation; + + private ListenerNotificationTask(ListenerInvocation listenerInvocation) { + this.listenersSnapshot = listeners.iterator(); + this.listenerInvocation = listenerInvocation; + } + + public void execute() { + while (listenersSnapshot.hasNext()) { + listenersSnapshot.next().invoke(listenerInvocation); + } + } + } +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastTimeline.java new file mode 100644 index 0000000000..115536ac4c --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastTimeline.java @@ -0,0 +1,342 @@ +/* + * 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.ext.cast; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.ShuffleOrder; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * A {@link Timeline} for Cast receiver app media queues. + * + *

      Each {@link MediaItem} in the timeline is exposed as a window. Unprepared media items are + * exposed as an unset-duration {@link Window}, with a single unset-duration {@link Period}. + */ +/* package */ final class ExoCastTimeline extends Timeline { + + /** Opaque object that uniquely identifies a period across timeline changes. */ + public interface PeriodUid {} + + /** A timeline for an empty media queue. */ + public static final ExoCastTimeline EMPTY = + createTimelineFor( + Collections.emptyList(), Collections.emptyMap(), new ShuffleOrder.DefaultShuffleOrder(0)); + + /** + * Creates {@link PeriodUid} from the given arguments. + * + * @param itemUuid The UUID that identifies the item. + * @param periodId The id of the period for which the unique identifier is required. + * @return An opaque unique identifier for a period. + */ + public static PeriodUid createPeriodUid(UUID itemUuid, Object periodId) { + return new PeriodUidImpl(itemUuid, periodId); + } + + /** + * Returns a new timeline representing the given media queue information. + * + * @param mediaItems The media items conforming the timeline. + * @param mediaItemInfoMap Maps {@link MediaItem media items} in {@code mediaItems} to a {@link + * MediaItemInfo} through their {@link MediaItem#uuid}. Media items may not have a {@link + * MediaItemInfo} mapped to them. + * @param shuffleOrder The {@link ShuffleOrder} of the timeline. {@link ShuffleOrder#getLength()} + * must be equal to {@code mediaItems.size()}. + * @return A new timeline representing the given media queue information. + */ + public static ExoCastTimeline createTimelineFor( + List mediaItems, + Map mediaItemInfoMap, + ShuffleOrder shuffleOrder) { + Assertions.checkArgument(mediaItems.size() == shuffleOrder.getLength()); + int[] accumulativePeriodCount = new int[mediaItems.size()]; + int periodCount = 0; + for (int i = 0; i < accumulativePeriodCount.length; i++) { + periodCount += getInfoOrEmpty(mediaItemInfoMap, mediaItems.get(i).uuid).periods.size(); + accumulativePeriodCount[i] = periodCount; + } + HashMap uuidToIndex = new HashMap<>(); + for (int i = 0; i < mediaItems.size(); i++) { + uuidToIndex.put(mediaItems.get(i).uuid, i); + } + return new ExoCastTimeline( + Collections.unmodifiableList(new ArrayList<>(mediaItems)), + Collections.unmodifiableMap(new HashMap<>(mediaItemInfoMap)), + Collections.unmodifiableMap(new HashMap<>(uuidToIndex)), + shuffleOrder, + accumulativePeriodCount); + } + + // Timeline backing information. + private final List mediaItems; + private final Map mediaItemInfoMap; + private final ShuffleOrder shuffleOrder; + + // Precomputed for quick access. + private final Map uuidToIndex; + private final int[] accumulativePeriodCount; + + private ExoCastTimeline( + List mediaItems, + Map mediaItemInfoMap, + Map uuidToIndex, + ShuffleOrder shuffleOrder, + int[] accumulativePeriodCount) { + this.mediaItems = mediaItems; + this.mediaItemInfoMap = mediaItemInfoMap; + this.uuidToIndex = uuidToIndex; + this.shuffleOrder = shuffleOrder; + this.accumulativePeriodCount = accumulativePeriodCount; + } + + /** + * Returns whether the given media queue information would produce a timeline equivalent to this + * one. + * + * @see ExoCastTimeline#createTimelineFor(List, Map, ShuffleOrder) + */ + public boolean representsMediaQueue( + List mediaItems, + Map mediaItemInfoMap, + ShuffleOrder shuffleOrder) { + if (this.shuffleOrder.getLength() != shuffleOrder.getLength()) { + return false; + } + + int index = shuffleOrder.getFirstIndex(); + if (this.shuffleOrder.getFirstIndex() != index) { + return false; + } + while (index != C.INDEX_UNSET) { + int nextIndex = shuffleOrder.getNextIndex(index); + if (nextIndex != this.shuffleOrder.getNextIndex(index)) { + return false; + } + index = nextIndex; + } + + if (mediaItems.size() != this.mediaItems.size()) { + return false; + } + for (int i = 0; i < mediaItems.size(); i++) { + UUID uuid = mediaItems.get(i).uuid; + MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, uuid); + if (!uuid.equals(this.mediaItems.get(i).uuid) + || !mediaItemInfo.equals(getInfoOrEmpty(this.mediaItemInfoMap, uuid))) { + return false; + } + } + return true; + } + + /** + * Returns the index of the window that contains the period identified by the given {@code + * periodUid} or {@link C#INDEX_UNSET} if this timeline does not contain any period with the given + * {@code periodUid}. + */ + public int getWindowIndexContainingPeriod(PeriodUid periodUid) { + if (!(periodUid instanceof PeriodUidImpl)) { + return C.INDEX_UNSET; + } + return getWindowIndexFromUuid(((PeriodUidImpl) periodUid).itemUuid); + } + + /** + * Returns the index of the window that represents the media item with the given {@code uuid} or + * {@link C#INDEX_UNSET} if no item in this timeline has the given {@code uuid}. + */ + public int getWindowIndexFromUuid(UUID uuid) { + Integer index = uuidToIndex.get(uuid); + return index != null ? index : C.INDEX_UNSET; + } + + // Timeline implementation. + + @Override + public int getWindowCount() { + return mediaItems.size(); + } + + @Override + public Window getWindow( + int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + MediaItem mediaItem = mediaItems.get(windowIndex); + MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, mediaItem.uuid); + return window.set( + /* tag= */ setTag ? mediaItem.attachment : null, + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + /* isSeekable= */ mediaItemInfo.isSeekable, + /* isDynamic= */ mediaItemInfo.isDynamic, + /* defaultPositionUs= */ mediaItemInfo.defaultStartPositionUs, + /* durationUs= */ mediaItemInfo.windowDurationUs, + /* firstPeriodIndex= */ windowIndex == 0 ? 0 : accumulativePeriodCount[windowIndex - 1], + /* lastPeriodIndex= */ accumulativePeriodCount[windowIndex] - 1, + mediaItemInfo.positionInFirstPeriodUs); + } + + @Override + public int getPeriodCount() { + return mediaItems.isEmpty() ? 0 : accumulativePeriodCount[accumulativePeriodCount.length - 1]; + } + + @Override + public Period getPeriodByUid(Object periodUidObject, Period period) { + return getPeriodInternal((PeriodUidImpl) periodUidObject, period, /* setIds= */ true); + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + return getPeriodInternal((PeriodUidImpl) getUidOfPeriod(periodIndex), period, setIds); + } + + @Override + public int getIndexOfPeriod(Object uid) { + if (!(uid instanceof PeriodUidImpl)) { + return C.INDEX_UNSET; + } + PeriodUidImpl periodUid = (PeriodUidImpl) uid; + UUID uuid = periodUid.itemUuid; + Integer itemIndex = uuidToIndex.get(uuid); + if (itemIndex == null) { + return C.INDEX_UNSET; + } + int indexOfPeriodInItem = + getInfoOrEmpty(mediaItemInfoMap, uuid).getIndexOfPeriod(periodUid.periodId); + if (indexOfPeriodInItem == C.INDEX_UNSET) { + return C.INDEX_UNSET; + } + return indexOfPeriodInItem + (itemIndex == 0 ? 0 : accumulativePeriodCount[itemIndex - 1]); + } + + @Override + public PeriodUid getUidOfPeriod(int periodIndex) { + int mediaItemIndex = getMediaItemIndexForPeriodIndex(periodIndex); + int periodIndexInMediaItem = + periodIndex - (mediaItemIndex > 0 ? accumulativePeriodCount[mediaItemIndex - 1] : 0); + UUID uuid = mediaItems.get(mediaItemIndex).uuid; + MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, uuid); + return new PeriodUidImpl(uuid, mediaItemInfo.periods.get(periodIndexInMediaItem).id); + } + + @Override + public int getFirstWindowIndex(boolean shuffleModeEnabled) { + return shuffleModeEnabled ? shuffleOrder.getFirstIndex() : 0; + } + + @Override + public int getLastWindowIndex(boolean shuffleModeEnabled) { + return shuffleModeEnabled ? shuffleOrder.getLastIndex() : mediaItems.size() - 1; + } + + @Override + public int getPreviousWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) { + if (repeatMode == Player.REPEAT_MODE_ONE) { + return windowIndex; + } else if (windowIndex == getFirstWindowIndex(shuffleModeEnabled)) { + return repeatMode == Player.REPEAT_MODE_OFF + ? C.INDEX_UNSET + : getLastWindowIndex(shuffleModeEnabled); + } else if (shuffleModeEnabled) { + return shuffleOrder.getPreviousIndex(windowIndex); + } else { + return windowIndex - 1; + } + } + + @Override + public int getNextWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) { + if (repeatMode == Player.REPEAT_MODE_ONE) { + return windowIndex; + } else if (windowIndex == getLastWindowIndex(shuffleModeEnabled)) { + return repeatMode == Player.REPEAT_MODE_OFF + ? C.INDEX_UNSET + : getFirstWindowIndex(shuffleModeEnabled); + } else if (shuffleModeEnabled) { + return shuffleOrder.getNextIndex(windowIndex); + } else { + return windowIndex + 1; + } + } + + // Internal methods. + + private Period getPeriodInternal(PeriodUidImpl uid, Period period, boolean setIds) { + UUID uuid = uid.itemUuid; + int itemIndex = Assertions.checkNotNull(uuidToIndex.get(uuid)); + MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, uuid); + MediaItemInfo.Period mediaInfoPeriod = + mediaItemInfo.periods.get(mediaItemInfo.getIndexOfPeriod(uid.periodId)); + return period.set( + setIds ? mediaInfoPeriod.id : null, + setIds ? uid : null, + /* windowIndex= */ itemIndex, + mediaInfoPeriod.durationUs, + mediaInfoPeriod.positionInWindowUs); + } + + private int getMediaItemIndexForPeriodIndex(int periodIndex) { + return Util.binarySearchCeil( + accumulativePeriodCount, periodIndex, /* inclusive= */ false, /* stayInBounds= */ false); + } + + private static MediaItemInfo getInfoOrEmpty(Map map, UUID uuid) { + MediaItemInfo info = map.get(uuid); + return info != null ? info : MediaItemInfo.EMPTY; + } + + // Internal classes. + + private static final class PeriodUidImpl implements PeriodUid { + + public final UUID itemUuid; + public final Object periodId; + + private PeriodUidImpl(UUID itemUuid, Object periodId) { + this.itemUuid = itemUuid; + this.periodId = periodId; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + PeriodUidImpl periodUid = (PeriodUidImpl) other; + return itemUuid.equals(periodUid.itemUuid) && periodId.equals(periodUid.periodId); + } + + @Override + public int hashCode() { + int result = itemUuid.hashCode(); + result = 31 * result + periodId.hashCode(); + return result; + } + } +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemInfo.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemInfo.java new file mode 100644 index 0000000000..cb5eff4f37 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemInfo.java @@ -0,0 +1,160 @@ +/* + * 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.ext.cast; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Util; +import java.util.Collections; +import java.util.List; + +// TODO (Internal b/119293631): Add ad playback state info. +/** + * Holds dynamic information for a {@link MediaItem}. + * + *

      Holds information related to preparation for a specific {@link MediaItem}. Unprepared items + * are associated with an {@link #EMPTY} info object until prepared. + */ +public final class MediaItemInfo { + + /** Placeholder information for media items that have not yet been prepared by the player. */ + public static final MediaItemInfo EMPTY = + new MediaItemInfo( + /* windowDurationUs= */ C.TIME_UNSET, + /* defaultStartPositionUs= */ 0L, + Collections.singletonList( + new Period( + /* id= */ new Object(), + /* durationUs= */ C.TIME_UNSET, + /* positionInWindowUs= */ 0L)), + /* positionInFirstPeriodUs= */ 0L, + /* isSeekable= */ false, + /* isDynamic= */ true); + + /** Holds the information of one of the periods of a {@link MediaItem}. */ + public static final class Period { + + /** + * The id of the period. Must be unique within the {@link MediaItem} but may match with periods + * in other items. + */ + public final Object id; + /** The duration of the period in microseconds. */ + public final long durationUs; + /** The position of this period in the window in microseconds. */ + public final long positionInWindowUs; + // TODO: Add track information. + + public Period(Object id, long durationUs, long positionInWindowUs) { + this.id = id; + this.durationUs = durationUs; + this.positionInWindowUs = positionInWindowUs; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + + Period period = (Period) other; + return durationUs == period.durationUs + && positionInWindowUs == period.positionInWindowUs + && id.equals(period.id); + } + + @Override + public int hashCode() { + int result = id.hashCode(); + result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); + result = 31 * result + (int) (positionInWindowUs ^ (positionInWindowUs >>> 32)); + return result; + } + } + + /** The duration of the window in microseconds. */ + public final long windowDurationUs; + /** The default start position relative to the start of the window, in microseconds. */ + public final long defaultStartPositionUs; + /** The periods conforming the media item. */ + public final List periods; + /** The position of the window in the first period in microseconds. */ + public final long positionInFirstPeriodUs; + /** Whether it is possible to seek within the window. */ + public final boolean isSeekable; + /** Whether the window may change when the timeline is updated. */ + public final boolean isDynamic; + + public MediaItemInfo( + long windowDurationUs, + long defaultStartPositionUs, + List periods, + long positionInFirstPeriodUs, + boolean isSeekable, + boolean isDynamic) { + this.windowDurationUs = windowDurationUs; + this.defaultStartPositionUs = defaultStartPositionUs; + this.periods = Collections.unmodifiableList(periods); + this.positionInFirstPeriodUs = positionInFirstPeriodUs; + this.isSeekable = isSeekable; + this.isDynamic = isDynamic; + } + + /** + * Returns the index of the period with {@link Period#id} equal to {@code periodId}, or {@link + * C#INDEX_UNSET} if none of the periods has the given id. + */ + public int getIndexOfPeriod(Object periodId) { + for (int i = 0; i < periods.size(); i++) { + if (Util.areEqual(periods.get(i).id, periodId)) { + return i; + } + } + return C.INDEX_UNSET; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + + MediaItemInfo that = (MediaItemInfo) other; + return windowDurationUs == that.windowDurationUs + && defaultStartPositionUs == that.defaultStartPositionUs + && positionInFirstPeriodUs == that.positionInFirstPeriodUs + && isSeekable == that.isSeekable + && isDynamic == that.isDynamic + && periods.equals(that.periods); + } + + @Override + public int hashCode() { + int result = (int) (windowDurationUs ^ (windowDurationUs >>> 32)); + result = 31 * result + (int) (defaultStartPositionUs ^ (defaultStartPositionUs >>> 32)); + result = 31 * result + periods.hashCode(); + result = 31 * result + (int) (positionInFirstPeriodUs ^ (positionInFirstPeriodUs >>> 32)); + result = 31 * result + (isSeekable ? 1 : 0); + result = 31 * result + (isDynamic ? 1 : 0); + return result; + } +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java new file mode 100644 index 0000000000..8cb6056340 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java @@ -0,0 +1,633 @@ +/* + * 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.ext.cast; + +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AD_INSERTION; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; +import static com.google.android.exoplayer2.Player.STATE_BUFFERING; +import static com.google.android.exoplayer2.Player.STATE_ENDED; +import static com.google.android.exoplayer2.Player.STATE_IDLE; +import static com.google.android.exoplayer2.Player.STATE_READY; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DEFAULT_START_POSITION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DESCRIPTION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISCONTINUITY_REASON; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DRM_SCHEMES; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DURATION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_END_POSITION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ERROR_MESSAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_DYNAMIC; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_LOADING; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_SEEKABLE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_LICENSE_SERVER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_ITEMS_INFO; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_QUEUE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MIME_TYPE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIODS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIOD_ID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_PARAMETERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_POSITION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_STATE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_IN_FIRST_PERIOD_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REQUEST_HEADERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_START_POSITION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TITLE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TRACK_SELECTION_PARAMETERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_URI; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_WINDOW_DURATION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_AD_INSERTION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_INTERNAL; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_PERIOD_TRANSITION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_SEEK; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_SEEK_ADJUSTMENT; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ONE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_BUFFERING; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_ENDED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_IDLE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_READY; + +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** Holds a playback state update from the receiver app. */ +public final class ReceiverAppStateUpdate { + + /** Builder for {@link ReceiverAppStateUpdate}. */ + public static final class Builder { + + private final long sequenceNumber; + @MonotonicNonNull private Boolean playWhenReady; + @MonotonicNonNull private Integer playbackState; + @MonotonicNonNull private List items; + @MonotonicNonNull private Integer repeatMode; + @MonotonicNonNull private Boolean shuffleModeEnabled; + @MonotonicNonNull private Boolean isLoading; + @MonotonicNonNull private PlaybackParameters playbackParameters; + @MonotonicNonNull private TrackSelectionParameters trackSelectionParameters; + @MonotonicNonNull private String errorMessage; + @MonotonicNonNull private Integer discontinuityReason; + @MonotonicNonNull private UUID currentPlayingItemUuid; + @MonotonicNonNull private String currentPlayingPeriodId; + @MonotonicNonNull private Long currentPlaybackPositionMs; + @MonotonicNonNull private List shuffleOrder; + private Map mediaItemsInformation; + + private Builder(long sequenceNumber) { + this.sequenceNumber = sequenceNumber; + mediaItemsInformation = Collections.emptyMap(); + } + + /** See {@link ReceiverAppStateUpdate#playWhenReady}. */ + public Builder setPlayWhenReady(Boolean playWhenReady) { + this.playWhenReady = playWhenReady; + return this; + } + + /** See {@link ReceiverAppStateUpdate#playbackState}. */ + public Builder setPlaybackState(Integer playbackState) { + this.playbackState = playbackState; + return this; + } + + /** See {@link ReceiverAppStateUpdate#items}. */ + public Builder setItems(List items) { + this.items = Collections.unmodifiableList(items); + return this; + } + + /** See {@link ReceiverAppStateUpdate#repeatMode}. */ + public Builder setRepeatMode(Integer repeatMode) { + this.repeatMode = repeatMode; + return this; + } + + /** See {@link ReceiverAppStateUpdate#shuffleModeEnabled}. */ + public Builder setShuffleModeEnabled(Boolean shuffleModeEnabled) { + this.shuffleModeEnabled = shuffleModeEnabled; + return this; + } + + /** See {@link ReceiverAppStateUpdate#isLoading}. */ + public Builder setIsLoading(Boolean isLoading) { + this.isLoading = isLoading; + return this; + } + + /** See {@link ReceiverAppStateUpdate#playbackParameters}. */ + public Builder setPlaybackParameters(PlaybackParameters playbackParameters) { + this.playbackParameters = playbackParameters; + return this; + } + + /** See {@link ReceiverAppStateUpdate#trackSelectionParameters} */ + public Builder setTrackSelectionParameters(TrackSelectionParameters trackSelectionParameters) { + this.trackSelectionParameters = trackSelectionParameters; + return this; + } + + /** See {@link ReceiverAppStateUpdate#errorMessage}. */ + public Builder setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + return this; + } + + /** See {@link ReceiverAppStateUpdate#discontinuityReason}. */ + public Builder setDiscontinuityReason(Integer discontinuityReason) { + this.discontinuityReason = discontinuityReason; + return this; + } + + /** + * See {@link ReceiverAppStateUpdate#currentPlayingItemUuid} and {@link + * ReceiverAppStateUpdate#currentPlaybackPositionMs}. + */ + public Builder setPlaybackPosition( + UUID currentPlayingItemUuid, + String currentPlayingPeriodId, + Long currentPlaybackPositionMs) { + this.currentPlayingItemUuid = currentPlayingItemUuid; + this.currentPlayingPeriodId = currentPlayingPeriodId; + this.currentPlaybackPositionMs = currentPlaybackPositionMs; + return this; + } + + /** + * See {@link ReceiverAppStateUpdate#currentPlayingItemUuid} and {@link + * ReceiverAppStateUpdate#currentPlaybackPositionMs}. + */ + public Builder setMediaItemsInformation(Map mediaItemsInformation) { + this.mediaItemsInformation = Collections.unmodifiableMap(mediaItemsInformation); + return this; + } + + /** See {@link ReceiverAppStateUpdate#shuffleOrder}. */ + public Builder setShuffleOrder(List shuffleOrder) { + this.shuffleOrder = Collections.unmodifiableList(shuffleOrder); + return this; + } + + /** + * Returns a new {@link ReceiverAppStateUpdate} instance with the current values in this + * builder. + */ + public ReceiverAppStateUpdate build() { + return new ReceiverAppStateUpdate( + sequenceNumber, + playWhenReady, + playbackState, + items, + repeatMode, + shuffleModeEnabled, + isLoading, + playbackParameters, + trackSelectionParameters, + errorMessage, + discontinuityReason, + currentPlayingItemUuid, + currentPlayingPeriodId, + currentPlaybackPositionMs, + mediaItemsInformation, + shuffleOrder); + } + } + + /** Returns a {@link ReceiverAppStateUpdate} builder. */ + public static Builder builder(long sequenceNumber) { + return new Builder(sequenceNumber); + } + + /** + * Creates an instance from parsing a state update received from the Receiver App. + * + * @param jsonMessage The state update encoded as a JSON string. + * @return The parsed state update. + * @throws JSONException If an error is encountered when parsing the {@code jsonMessage}. + */ + public static ReceiverAppStateUpdate fromJsonMessage(String jsonMessage) throws JSONException { + JSONObject stateAsJson = new JSONObject(jsonMessage); + Builder builder = builder(stateAsJson.getLong(KEY_SEQUENCE_NUMBER)); + + if (stateAsJson.has(KEY_PLAY_WHEN_READY)) { + builder.setPlayWhenReady(stateAsJson.getBoolean(KEY_PLAY_WHEN_READY)); + } + + if (stateAsJson.has(KEY_PLAYBACK_STATE)) { + builder.setPlaybackState( + playbackStateStringToConstant(stateAsJson.getString(KEY_PLAYBACK_STATE))); + } + + if (stateAsJson.has(KEY_MEDIA_QUEUE)) { + builder.setItems( + toMediaItemArrayList(Assertions.checkNotNull(stateAsJson.optJSONArray(KEY_MEDIA_QUEUE)))); + } + + if (stateAsJson.has(KEY_REPEAT_MODE)) { + builder.setRepeatMode(stringToRepeatMode(stateAsJson.getString(KEY_REPEAT_MODE))); + } + + if (stateAsJson.has(KEY_SHUFFLE_MODE_ENABLED)) { + builder.setShuffleModeEnabled(stateAsJson.getBoolean(KEY_SHUFFLE_MODE_ENABLED)); + } + + if (stateAsJson.has(KEY_IS_LOADING)) { + builder.setIsLoading(stateAsJson.getBoolean(KEY_IS_LOADING)); + } + + if (stateAsJson.has(KEY_PLAYBACK_PARAMETERS)) { + builder.setPlaybackParameters( + toPlaybackParameters( + Assertions.checkNotNull(stateAsJson.optJSONObject(KEY_PLAYBACK_PARAMETERS)))); + } + + if (stateAsJson.has(KEY_TRACK_SELECTION_PARAMETERS)) { + JSONObject trackSelectionParametersJson = + stateAsJson.getJSONObject(KEY_TRACK_SELECTION_PARAMETERS); + TrackSelectionParameters parameters = + TrackSelectionParameters.DEFAULT + .buildUpon() + .setPreferredTextLanguage( + trackSelectionParametersJson.getString(KEY_PREFERRED_TEXT_LANGUAGE)) + .setPreferredAudioLanguage( + trackSelectionParametersJson.getString(KEY_PREFERRED_AUDIO_LANGUAGE)) + .setSelectUndeterminedTextLanguage( + trackSelectionParametersJson.getBoolean(KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE)) + .setDisabledTextTrackSelectionFlags( + jsonArrayToSelectionFlags( + trackSelectionParametersJson.getJSONArray( + KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS))) + .build(); + builder.setTrackSelectionParameters(parameters); + } + + if (stateAsJson.has(KEY_ERROR_MESSAGE)) { + builder.setErrorMessage(stateAsJson.getString(KEY_ERROR_MESSAGE)); + } + + if (stateAsJson.has(KEY_PLAYBACK_POSITION)) { + JSONObject playbackPosition = stateAsJson.getJSONObject(KEY_PLAYBACK_POSITION); + String discontinuityReason = playbackPosition.optString(KEY_DISCONTINUITY_REASON); + if (!discontinuityReason.isEmpty()) { + builder.setDiscontinuityReason(stringToDiscontinuityReason(discontinuityReason)); + } + UUID currentPlayingItemUuid = UUID.fromString(playbackPosition.getString(KEY_UUID)); + String currentPlayingPeriodId = playbackPosition.getString(KEY_PERIOD_ID); + Long currentPlaybackPositionMs = playbackPosition.getLong(KEY_POSITION_MS); + builder.setPlaybackPosition( + currentPlayingItemUuid, currentPlayingPeriodId, currentPlaybackPositionMs); + } + + if (stateAsJson.has(KEY_MEDIA_ITEMS_INFO)) { + HashMap mediaItemInformation = new HashMap<>(); + JSONObject mediaItemsInfo = stateAsJson.getJSONObject(KEY_MEDIA_ITEMS_INFO); + for (Iterator i = mediaItemsInfo.keys(); i.hasNext(); ) { + String key = i.next(); + mediaItemInformation.put( + UUID.fromString(key), jsonToMediaitemInfo(mediaItemsInfo.getJSONObject(key))); + } + builder.setMediaItemsInformation(mediaItemInformation); + } + + if (stateAsJson.has(KEY_SHUFFLE_ORDER)) { + ArrayList shuffleOrder = new ArrayList<>(); + JSONArray shuffleOrderJson = stateAsJson.getJSONArray(KEY_SHUFFLE_ORDER); + for (int i = 0; i < shuffleOrderJson.length(); i++) { + shuffleOrder.add(shuffleOrderJson.getInt(i)); + } + builder.setShuffleOrder(shuffleOrder); + } + + return builder.build(); + } + + /** The sequence number of the status update. */ + public final long sequenceNumber; + /** Optional {@link Player#getPlayWhenReady playWhenReady} value. */ + @Nullable public final Boolean playWhenReady; + /** Optional {@link Player#getPlaybackState() playbackState}. */ + @Nullable public final Integer playbackState; + /** Optional list of media items. */ + @Nullable public final List items; + /** Optional {@link Player#getRepeatMode() repeatMode}. */ + @Nullable public final Integer repeatMode; + /** Optional {@link Player#getShuffleModeEnabled() shuffleMode}. */ + @Nullable public final Boolean shuffleModeEnabled; + /** Optional {@link Player#isLoading() isLoading} value. */ + @Nullable public final Boolean isLoading; + /** Optional {@link Player#getPlaybackParameters() playbackParameters}. */ + @Nullable public final PlaybackParameters playbackParameters; + /** Optional {@link TrackSelectionParameters}. */ + @Nullable public final TrackSelectionParameters trackSelectionParameters; + /** Optional error message string. */ + @Nullable public final String errorMessage; + /** + * Optional reason for a {@link Player.EventListener#onPositionDiscontinuity(int) discontinuity } + * in the playback position. + */ + @Nullable public final Integer discontinuityReason; + /** Optional {@link UUID} of the {@link Player#getCurrentWindowIndex() currently played item}. */ + @Nullable public final UUID currentPlayingItemUuid; + /** Optional id of the current {@link Player#getCurrentPeriodIndex() period being played}. */ + @Nullable public final String currentPlayingPeriodId; + /** Optional {@link Player#getCurrentPosition() playbackPosition} in milliseconds. */ + @Nullable public final Long currentPlaybackPositionMs; + /** Holds information about the {@link MediaItem media items} in the media queue. */ + public final Map mediaItemsInformation; + /** Holds the indices of the media queue items in shuffle order. */ + @Nullable public final List shuffleOrder; + + /** Creates an instance with the given values. */ + private ReceiverAppStateUpdate( + long sequenceNumber, + @Nullable Boolean playWhenReady, + @Nullable Integer playbackState, + @Nullable List items, + @Nullable Integer repeatMode, + @Nullable Boolean shuffleModeEnabled, + @Nullable Boolean isLoading, + @Nullable PlaybackParameters playbackParameters, + @Nullable TrackSelectionParameters trackSelectionParameters, + @Nullable String errorMessage, + @Nullable Integer discontinuityReason, + @Nullable UUID currentPlayingItemUuid, + @Nullable String currentPlayingPeriodId, + @Nullable Long currentPlaybackPositionMs, + Map mediaItemsInformation, + @Nullable List shuffleOrder) { + this.sequenceNumber = sequenceNumber; + this.playWhenReady = playWhenReady; + this.playbackState = playbackState; + this.items = items; + this.repeatMode = repeatMode; + this.shuffleModeEnabled = shuffleModeEnabled; + this.isLoading = isLoading; + this.playbackParameters = playbackParameters; + this.trackSelectionParameters = trackSelectionParameters; + this.errorMessage = errorMessage; + this.discontinuityReason = discontinuityReason; + this.currentPlayingItemUuid = currentPlayingItemUuid; + this.currentPlayingPeriodId = currentPlayingPeriodId; + this.currentPlaybackPositionMs = currentPlaybackPositionMs; + this.mediaItemsInformation = mediaItemsInformation; + this.shuffleOrder = shuffleOrder; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + ReceiverAppStateUpdate that = (ReceiverAppStateUpdate) other; + + return sequenceNumber == that.sequenceNumber + && Util.areEqual(playWhenReady, that.playWhenReady) + && Util.areEqual(playbackState, that.playbackState) + && Util.areEqual(items, that.items) + && Util.areEqual(repeatMode, that.repeatMode) + && Util.areEqual(shuffleModeEnabled, that.shuffleModeEnabled) + && Util.areEqual(isLoading, that.isLoading) + && Util.areEqual(playbackParameters, that.playbackParameters) + && Util.areEqual(trackSelectionParameters, that.trackSelectionParameters) + && Util.areEqual(errorMessage, that.errorMessage) + && Util.areEqual(discontinuityReason, that.discontinuityReason) + && Util.areEqual(currentPlayingItemUuid, that.currentPlayingItemUuid) + && Util.areEqual(currentPlayingPeriodId, that.currentPlayingPeriodId) + && Util.areEqual(currentPlaybackPositionMs, that.currentPlaybackPositionMs) + && Util.areEqual(mediaItemsInformation, that.mediaItemsInformation) + && Util.areEqual(shuffleOrder, that.shuffleOrder); + } + + @Override + public int hashCode() { + int result = (int) (sequenceNumber ^ (sequenceNumber >>> 32)); + result = 31 * result + (playWhenReady != null ? playWhenReady.hashCode() : 0); + result = 31 * result + (playbackState != null ? playbackState.hashCode() : 0); + result = 31 * result + (items != null ? items.hashCode() : 0); + result = 31 * result + (repeatMode != null ? repeatMode.hashCode() : 0); + result = 31 * result + (shuffleModeEnabled != null ? shuffleModeEnabled.hashCode() : 0); + result = 31 * result + (isLoading != null ? isLoading.hashCode() : 0); + result = 31 * result + (playbackParameters != null ? playbackParameters.hashCode() : 0); + result = + 31 * result + (trackSelectionParameters != null ? trackSelectionParameters.hashCode() : 0); + result = 31 * result + (errorMessage != null ? errorMessage.hashCode() : 0); + result = 31 * result + (discontinuityReason != null ? discontinuityReason.hashCode() : 0); + result = 31 * result + (currentPlayingItemUuid != null ? currentPlayingItemUuid.hashCode() : 0); + result = 31 * result + (currentPlayingPeriodId != null ? currentPlayingPeriodId.hashCode() : 0); + result = + 31 * result + + (currentPlaybackPositionMs != null ? currentPlaybackPositionMs.hashCode() : 0); + result = 31 * result + mediaItemsInformation.hashCode(); + result = 31 * result + (shuffleOrder != null ? shuffleOrder.hashCode() : 0); + return result; + } + + // Internal methods. + + @VisibleForTesting + /* package */ static List toMediaItemArrayList(JSONArray mediaItemsAsJson) + throws JSONException { + ArrayList mediaItems = new ArrayList<>(); + for (int i = 0; i < mediaItemsAsJson.length(); i++) { + mediaItems.add(toMediaItem(mediaItemsAsJson.getJSONObject(i))); + } + return mediaItems; + } + + private static MediaItem toMediaItem(JSONObject mediaItemAsJson) throws JSONException { + MediaItem.Builder builder = new MediaItem.Builder(); + builder.setUuid(UUID.fromString(mediaItemAsJson.getString(KEY_UUID))); + builder.setTitle(mediaItemAsJson.getString(KEY_TITLE)); + builder.setDescription(mediaItemAsJson.getString(KEY_DESCRIPTION)); + builder.setMedia(jsonToUriBundle(mediaItemAsJson.getJSONObject(KEY_MEDIA))); + // TODO(Internal b/118431961): Add attachment management. + + builder.setDrmSchemes(jsonArrayToDrmSchemes(mediaItemAsJson.getJSONArray(KEY_DRM_SCHEMES))); + if (mediaItemAsJson.has(KEY_START_POSITION_US)) { + builder.setStartPositionUs(mediaItemAsJson.getLong(KEY_START_POSITION_US)); + } + if (mediaItemAsJson.has(KEY_END_POSITION_US)) { + builder.setEndPositionUs(mediaItemAsJson.getLong(KEY_END_POSITION_US)); + } + builder.setMimeType(mediaItemAsJson.getString(KEY_MIME_TYPE)); + return builder.build(); + } + + private static PlaybackParameters toPlaybackParameters(JSONObject parameters) + throws JSONException { + float speed = (float) parameters.getDouble(KEY_SPEED); + float pitch = (float) parameters.getDouble(KEY_PITCH); + boolean skipSilence = parameters.getBoolean(KEY_SKIP_SILENCE); + return new PlaybackParameters(speed, pitch, skipSilence); + } + + private static int playbackStateStringToConstant(String string) { + switch (string) { + case STR_STATE_IDLE: + return STATE_IDLE; + case STR_STATE_BUFFERING: + return STATE_BUFFERING; + case STR_STATE_READY: + return STATE_READY; + case STR_STATE_ENDED: + return STATE_ENDED; + default: + throw new AssertionError("Unexpected state string: " + string); + } + } + + private static Integer stringToRepeatMode(String repeatModeStr) { + switch (repeatModeStr) { + case STR_REPEAT_MODE_OFF: + return REPEAT_MODE_OFF; + case STR_REPEAT_MODE_ONE: + return REPEAT_MODE_ONE; + case STR_REPEAT_MODE_ALL: + return REPEAT_MODE_ALL; + default: + throw new AssertionError("Illegal repeat mode: " + repeatModeStr); + } + } + + private static Integer stringToDiscontinuityReason(String discontinuityReasonStr) { + switch (discontinuityReasonStr) { + case STR_DISCONTINUITY_REASON_PERIOD_TRANSITION: + return DISCONTINUITY_REASON_PERIOD_TRANSITION; + case STR_DISCONTINUITY_REASON_SEEK: + return DISCONTINUITY_REASON_SEEK; + case STR_DISCONTINUITY_REASON_SEEK_ADJUSTMENT: + return DISCONTINUITY_REASON_SEEK_ADJUSTMENT; + case STR_DISCONTINUITY_REASON_AD_INSERTION: + return DISCONTINUITY_REASON_AD_INSERTION; + case STR_DISCONTINUITY_REASON_INTERNAL: + return DISCONTINUITY_REASON_INTERNAL; + default: + throw new AssertionError("Illegal discontinuity reason: " + discontinuityReasonStr); + } + } + + @C.SelectionFlags + private static int jsonArrayToSelectionFlags(JSONArray array) throws JSONException { + int result = 0; + for (int i = 0; i < array.length(); i++) { + switch (array.getString(i)) { + case ExoCastConstants.STR_SELECTION_FLAG_AUTOSELECT: + result |= C.SELECTION_FLAG_AUTOSELECT; + break; + case ExoCastConstants.STR_SELECTION_FLAG_FORCED: + result |= C.SELECTION_FLAG_FORCED; + break; + case ExoCastConstants.STR_SELECTION_FLAG_DEFAULT: + result |= C.SELECTION_FLAG_DEFAULT; + break; + default: + // Do nothing. + break; + } + } + return result; + } + + private static List jsonArrayToDrmSchemes(JSONArray drmSchemesAsJson) + throws JSONException { + ArrayList drmSchemes = new ArrayList<>(); + for (int i = 0; i < drmSchemesAsJson.length(); i++) { + JSONObject drmSchemeAsJson = drmSchemesAsJson.getJSONObject(i); + MediaItem.UriBundle uriBundle = + drmSchemeAsJson.has(KEY_LICENSE_SERVER) + ? jsonToUriBundle(drmSchemeAsJson.getJSONObject(KEY_LICENSE_SERVER)) + : null; + drmSchemes.add( + new MediaItem.DrmScheme(UUID.fromString(drmSchemeAsJson.getString(KEY_UUID)), uriBundle)); + } + return Collections.unmodifiableList(drmSchemes); + } + + private static MediaItem.UriBundle jsonToUriBundle(JSONObject json) throws JSONException { + Uri uri = Uri.parse(json.getString(KEY_URI)); + JSONObject requestHeadersAsJson = json.getJSONObject(KEY_REQUEST_HEADERS); + HashMap requestHeaders = new HashMap<>(); + for (Iterator i = requestHeadersAsJson.keys(); i.hasNext(); ) { + String key = i.next(); + requestHeaders.put(key, requestHeadersAsJson.getString(key)); + } + return new MediaItem.UriBundle(uri, requestHeaders); + } + + private static MediaItemInfo jsonToMediaitemInfo(JSONObject json) throws JSONException { + long durationUs = json.getLong(KEY_WINDOW_DURATION_US); + long defaultPositionUs = json.optLong(KEY_DEFAULT_START_POSITION_US, /* fallback= */ 0L); + JSONArray periodsJson = json.getJSONArray(KEY_PERIODS); + ArrayList periods = new ArrayList<>(); + long positionInFirstPeriodUs = json.getLong(KEY_POSITION_IN_FIRST_PERIOD_US); + + long windowPositionUs = -positionInFirstPeriodUs; + for (int i = 0; i < periodsJson.length(); i++) { + JSONObject periodJson = periodsJson.getJSONObject(i); + long periodDurationUs = periodJson.optLong(KEY_DURATION_US, C.TIME_UNSET); + periods.add( + new MediaItemInfo.Period( + periodJson.getString(KEY_ID), periodDurationUs, windowPositionUs)); + windowPositionUs += periodDurationUs; + } + boolean isDynamic = json.getBoolean(KEY_IS_DYNAMIC); + boolean isSeekable = json.getBoolean(KEY_IS_SEEKABLE); + return new MediaItemInfo( + durationUs, defaultPositionUs, periods, positionInFirstPeriodUs, isSeekable, isDynamic); + } +} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastMessageTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastMessageTest.java new file mode 100644 index 0000000000..b900a78937 --- /dev/null +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastMessageTest.java @@ -0,0 +1,436 @@ +/* + * 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.ext.cast; + +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ARGS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DESCRIPTION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DRM_SCHEMES; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_END_POSITION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_INDEX; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ITEMS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_LICENSE_SERVER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_METHOD; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MIME_TYPE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REQUEST_HEADERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_START_POSITION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TITLE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_URI; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUIDS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_ADD_ITEMS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_MOVE_ITEM; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_REMOVE_ITEMS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SEEK_TO; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAYBACK_PARAMETERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAY_WHEN_READY; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_REPEAT_MODE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_SHUFFLE_MODE_ENABLED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_TRACK_SELECTION_PARAMETERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ONE; +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ext.cast.MediaItem.DrmScheme; +import com.google.android.exoplayer2.ext.cast.MediaItem.UriBundle; +import com.google.android.exoplayer2.source.ShuffleOrder; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link ExoCastMessage}. */ +@RunWith(AndroidJUnit4.class) +public class ExoCastMessageTest { + + @Test + public void addItems_withUnsetIndex_doesNotAddIndexToJson() throws JSONException { + MediaItem sampleItem = new MediaItem.Builder().build(); + ExoCastMessage message = + new ExoCastMessage.AddItems( + C.INDEX_UNSET, + Collections.singletonList(sampleItem), + new ShuffleOrder.UnshuffledShuffleOrder(1)); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + JSONArray items = arguments.getJSONArray(KEY_ITEMS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_ADD_ITEMS); + assertThat(arguments.has(KEY_INDEX)).isFalse(); + assertThat(items.length()).isEqualTo(1); + } + + @Test + public void addItems_withMultipleItems_producesExpectedJsonList() throws JSONException { + MediaItem sampleItem1 = new MediaItem.Builder().build(); + MediaItem sampleItem2 = new MediaItem.Builder().build(); + ExoCastMessage message = + new ExoCastMessage.AddItems( + 1, Arrays.asList(sampleItem2, sampleItem1), new ShuffleOrder.UnshuffledShuffleOrder(2)); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 1)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + JSONArray items = arguments.getJSONArray(KEY_ITEMS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(1); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_ADD_ITEMS); + assertThat(arguments.getInt(KEY_INDEX)).isEqualTo(1); + assertThat(items.length()).isEqualTo(2); + } + + @Test + public void addItems_withoutItemOptionalFields_doesNotAddFieldsToJson() throws JSONException { + MediaItem itemWithoutOptionalFields = + new MediaItem.Builder() + .setTitle("title") + .setMimeType(MimeTypes.AUDIO_MP4) + .setDescription("desc") + .setDrmSchemes(Collections.singletonList(new DrmScheme(C.WIDEVINE_UUID, null))) + .setMedia("www.google.com") + .build(); + ExoCastMessage message = + new ExoCastMessage.AddItems( + C.INDEX_UNSET, + Collections.singletonList(itemWithoutOptionalFields), + new ShuffleOrder.UnshuffledShuffleOrder(1)); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + JSONArray items = arguments.getJSONArray(KEY_ITEMS); + + assertJsonEqualsMediaItem(items.getJSONObject(/* index= */ 0), itemWithoutOptionalFields); + } + + @Test + public void addItems_withAllItemFields_addsFieldsToJson() throws JSONException { + HashMap headersMedia = new HashMap<>(); + headersMedia.put("header1", "value1"); + headersMedia.put("header2", "value2"); + UriBundle media = new UriBundle(Uri.parse("www.google.com"), headersMedia); + + HashMap headersWidevine = new HashMap<>(); + headersWidevine.put("widevine", "value"); + UriBundle widevingUriBundle = new UriBundle(Uri.parse("www.widevine.com"), headersWidevine); + + HashMap headersPlayready = new HashMap<>(); + headersPlayready.put("playready", "value"); + UriBundle playreadyUriBundle = new UriBundle(Uri.parse("www.playready.com"), headersPlayready); + + DrmScheme[] drmSchemes = + new DrmScheme[] { + new DrmScheme(C.WIDEVINE_UUID, widevingUriBundle), + new DrmScheme(C.PLAYREADY_UUID, playreadyUriBundle) + }; + MediaItem itemWithAllFields = + new MediaItem.Builder() + .setTitle("title") + .setMimeType(MimeTypes.VIDEO_MP4) + .setDescription("desc") + .setStartPositionUs(3) + .setEndPositionUs(10) + .setDrmSchemes(Arrays.asList(drmSchemes)) + .setMedia(media) + .build(); + ExoCastMessage message = + new ExoCastMessage.AddItems( + C.INDEX_UNSET, + Collections.singletonList(itemWithAllFields), + new ShuffleOrder.UnshuffledShuffleOrder(1)); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + JSONArray items = arguments.getJSONArray(KEY_ITEMS); + + assertJsonEqualsMediaItem(items.getJSONObject(/* index= */ 0), itemWithAllFields); + } + + @Test + public void addItems_withShuffleOrder_producesExpectedJson() throws JSONException { + MediaItem.Builder builder = new MediaItem.Builder(); + MediaItem sampleItem1 = builder.build(); + MediaItem sampleItem2 = builder.build(); + MediaItem sampleItem3 = builder.build(); + MediaItem sampleItem4 = builder.build(); + + ExoCastMessage message = + new ExoCastMessage.AddItems( + C.INDEX_UNSET, + Arrays.asList(sampleItem1, sampleItem2, sampleItem3, sampleItem4), + new ShuffleOrder.DefaultShuffleOrder(new int[] {2, 1, 3, 0}, /* randomSeed= */ 0)); + JSONObject arguments = + new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)).getJSONObject(KEY_ARGS); + JSONArray shuffledIndices = arguments.getJSONArray(KEY_SHUFFLE_ORDER); + assertThat(shuffledIndices.getInt(0)).isEqualTo(2); + assertThat(shuffledIndices.getInt(1)).isEqualTo(1); + assertThat(shuffledIndices.getInt(2)).isEqualTo(3); + assertThat(shuffledIndices.getInt(3)).isEqualTo(0); + } + + @Test + public void moveItem_producesExpectedJson() throws JSONException { + ExoCastMessage message = + new ExoCastMessage.MoveItem( + new UUID(0, 1), + /* index= */ 3, + new ShuffleOrder.DefaultShuffleOrder(new int[] {2, 1, 3, 0}, /* randomSeed= */ 0)); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 1)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(1); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_MOVE_ITEM); + assertThat(arguments.getString(KEY_UUID)).isEqualTo(new UUID(0, 1).toString()); + assertThat(arguments.getInt(KEY_INDEX)).isEqualTo(3); + JSONArray shuffledIndices = arguments.getJSONArray(KEY_SHUFFLE_ORDER); + assertThat(shuffledIndices.getInt(0)).isEqualTo(2); + assertThat(shuffledIndices.getInt(1)).isEqualTo(1); + assertThat(shuffledIndices.getInt(2)).isEqualTo(3); + assertThat(shuffledIndices.getInt(3)).isEqualTo(0); + } + + @Test + public void removeItems_withSingleItem_producesExpectedJson() throws JSONException { + ExoCastMessage message = + new ExoCastMessage.RemoveItems(Collections.singletonList(new UUID(0, 1))); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONArray uuids = messageAsJson.getJSONObject(KEY_ARGS).getJSONArray(KEY_UUIDS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_REMOVE_ITEMS); + assertThat(uuids.length()).isEqualTo(1); + assertThat(uuids.getString(0)).isEqualTo(new UUID(0, 1).toString()); + } + + @Test + public void removeItems_withMultipleItems_producesExpectedJson() throws JSONException { + ExoCastMessage message = + new ExoCastMessage.RemoveItems( + Arrays.asList(new UUID(0, 1), new UUID(0, 2), new UUID(0, 3))); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONArray uuids = messageAsJson.getJSONObject(KEY_ARGS).getJSONArray(KEY_UUIDS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_REMOVE_ITEMS); + assertThat(uuids.length()).isEqualTo(3); + assertThat(uuids.getString(0)).isEqualTo(new UUID(0, 1).toString()); + assertThat(uuids.getString(1)).isEqualTo(new UUID(0, 2).toString()); + assertThat(uuids.getString(2)).isEqualTo(new UUID(0, 3).toString()); + } + + @Test + public void setPlayWhenReady_producesExpectedJson() throws JSONException { + ExoCastMessage message = new ExoCastMessage.SetPlayWhenReady(true); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_PLAY_WHEN_READY); + assertThat(messageAsJson.getJSONObject(KEY_ARGS).getBoolean(KEY_PLAY_WHEN_READY)).isTrue(); + } + + @Test + public void setRepeatMode_withRepeatModeOff_producesExpectedJson() throws JSONException { + ExoCastMessage message = new ExoCastMessage.SetRepeatMode(Player.REPEAT_MODE_OFF); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_REPEAT_MODE); + assertThat(messageAsJson.getJSONObject(KEY_ARGS).getString(KEY_REPEAT_MODE)) + .isEqualTo(STR_REPEAT_MODE_OFF); + } + + @Test + public void setRepeatMode_withRepeatModeOne_producesExpectedJson() throws JSONException { + ExoCastMessage message = new ExoCastMessage.SetRepeatMode(Player.REPEAT_MODE_ONE); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_REPEAT_MODE); + assertThat(messageAsJson.getJSONObject(KEY_ARGS).getString(KEY_REPEAT_MODE)) + .isEqualTo(STR_REPEAT_MODE_ONE); + } + + @Test + public void setRepeatMode_withRepeatModeAll_producesExpectedJson() throws JSONException { + ExoCastMessage message = new ExoCastMessage.SetRepeatMode(Player.REPEAT_MODE_ALL); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_REPEAT_MODE); + assertThat(messageAsJson.getJSONObject(KEY_ARGS).getString(KEY_REPEAT_MODE)) + .isEqualTo(STR_REPEAT_MODE_ALL); + } + + @Test + public void setShuffleModeEnabled_producesExpectedJson() throws JSONException { + ExoCastMessage message = + new ExoCastMessage.SetShuffleModeEnabled(/* shuffleModeEnabled= */ false); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_SHUFFLE_MODE_ENABLED); + assertThat(messageAsJson.getJSONObject(KEY_ARGS).getBoolean(KEY_SHUFFLE_MODE_ENABLED)) + .isFalse(); + } + + @Test + public void seekTo_withPositionInItem_addsPositionField() throws JSONException { + ExoCastMessage message = new ExoCastMessage.SeekTo(new UUID(0, 1), /* positionMs= */ 10); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SEEK_TO); + assertThat(arguments.getString(KEY_UUID)).isEqualTo(new UUID(0, 1).toString()); + assertThat(arguments.getLong(KEY_POSITION_MS)).isEqualTo(10); + } + + @Test + public void seekTo_withUnsetPosition_doesNotAddPositionField() throws JSONException { + ExoCastMessage message = + new ExoCastMessage.SeekTo(new UUID(0, 1), /* positionMs= */ C.TIME_UNSET); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SEEK_TO); + assertThat(arguments.getString(KEY_UUID)).isEqualTo(new UUID(0, 1).toString()); + assertThat(arguments.has(KEY_POSITION_MS)).isFalse(); + } + + @Test + public void setPlaybackParameters_producesExpectedJson() throws JSONException { + ExoCastMessage message = + new ExoCastMessage.SetPlaybackParameters( + new PlaybackParameters(/* speed= */ 0.5f, /* pitch= */ 2, /* skipSilence= */ false)); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_PLAYBACK_PARAMETERS); + assertThat(arguments.getDouble(KEY_SPEED)).isEqualTo(0.5); + assertThat(arguments.getDouble(KEY_PITCH)).isEqualTo(2.0); + assertThat(arguments.getBoolean(KEY_SKIP_SILENCE)).isFalse(); + } + + @Test + public void setSelectionParameters_producesExpectedJson() throws JSONException { + ExoCastMessage message = + new ExoCastMessage.SetTrackSelectionParameters( + TrackSelectionParameters.DEFAULT + .buildUpon() + .setDisabledTextTrackSelectionFlags( + C.SELECTION_FLAG_AUTOSELECT | C.SELECTION_FLAG_DEFAULT) + .setSelectUndeterminedTextLanguage(true) + .setPreferredAudioLanguage("esp") + .setPreferredTextLanguage("deu") + .build()); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)) + .isEqualTo(METHOD_SET_TRACK_SELECTION_PARAMETERS); + assertThat(arguments.getBoolean(KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE)).isTrue(); + assertThat(arguments.getString(KEY_PREFERRED_AUDIO_LANGUAGE)).isEqualTo("esp"); + assertThat(arguments.getString(KEY_PREFERRED_TEXT_LANGUAGE)).isEqualTo("deu"); + ArrayList selectionFlagStrings = new ArrayList<>(); + JSONArray selectionFlagsJson = arguments.getJSONArray(KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS); + for (int i = 0; i < selectionFlagsJson.length(); i++) { + selectionFlagStrings.add(selectionFlagsJson.getString(i)); + } + assertThat(selectionFlagStrings).contains(ExoCastConstants.STR_SELECTION_FLAG_AUTOSELECT); + assertThat(selectionFlagStrings).doesNotContain(ExoCastConstants.STR_SELECTION_FLAG_FORCED); + assertThat(selectionFlagStrings).contains(ExoCastConstants.STR_SELECTION_FLAG_DEFAULT); + } + + private static void assertJsonEqualsMediaItem(JSONObject itemAsJson, MediaItem mediaItem) + throws JSONException { + assertThat(itemAsJson.getString(KEY_UUID)).isEqualTo(mediaItem.uuid.toString()); + assertThat(itemAsJson.getString(KEY_TITLE)).isEqualTo(mediaItem.title); + assertThat(itemAsJson.getString(KEY_MIME_TYPE)).isEqualTo(mediaItem.mimeType); + assertThat(itemAsJson.getString(KEY_DESCRIPTION)).isEqualTo(mediaItem.description); + assertJsonMatchesTimestamp(itemAsJson, KEY_START_POSITION_US, mediaItem.startPositionUs); + assertJsonMatchesTimestamp(itemAsJson, KEY_END_POSITION_US, mediaItem.endPositionUs); + assertJsonMatchesUriBundle(itemAsJson, KEY_MEDIA, mediaItem.media); + + List drmSchemes = mediaItem.drmSchemes; + int drmSchemesLength = drmSchemes.size(); + JSONArray drmSchemesAsJson = itemAsJson.getJSONArray(KEY_DRM_SCHEMES); + + assertThat(drmSchemesAsJson.length()).isEqualTo(drmSchemesLength); + for (int i = 0; i < drmSchemesLength; i++) { + DrmScheme drmScheme = drmSchemes.get(i); + JSONObject drmSchemeAsJson = drmSchemesAsJson.getJSONObject(i); + + assertThat(drmSchemeAsJson.getString(KEY_UUID)).isEqualTo(drmScheme.uuid.toString()); + assertJsonMatchesUriBundle(drmSchemeAsJson, KEY_LICENSE_SERVER, drmScheme.licenseServer); + } + } + + private static void assertJsonMatchesUriBundle( + JSONObject jsonObject, String key, @Nullable UriBundle uriBundle) throws JSONException { + if (uriBundle == null) { + assertThat(jsonObject.has(key)).isFalse(); + return; + } + JSONObject uriBundleAsJson = jsonObject.getJSONObject(key); + assertThat(uriBundleAsJson.getString(KEY_URI)).isEqualTo(uriBundle.uri.toString()); + Map requestHeaders = uriBundle.requestHeaders; + JSONObject requestHeadersAsJson = uriBundleAsJson.getJSONObject(KEY_REQUEST_HEADERS); + + assertThat(requestHeadersAsJson.length()).isEqualTo(requestHeaders.size()); + for (String headerKey : requestHeaders.keySet()) { + assertThat(requestHeadersAsJson.getString(headerKey)) + .isEqualTo(requestHeaders.get(headerKey)); + } + } + + private static void assertJsonMatchesTimestamp(JSONObject object, String key, long timestamp) + throws JSONException { + if (timestamp == C.TIME_UNSET) { + assertThat(object.has(key)).isFalse(); + } else { + assertThat(object.getLong(key)).isEqualTo(timestamp); + } + } +} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayerTest.java new file mode 100644 index 0000000000..58f78b090a --- /dev/null +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayerTest.java @@ -0,0 +1,1018 @@ +/* + * 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.ext.cast; + +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ARGS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_INDEX; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ITEMS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUIDS; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isNull; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.testutil.FakeClock; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; + +/** Unit test for {@link ExoCastPlayer}. */ +@RunWith(AndroidJUnit4.class) +public class ExoCastPlayerTest { + + private static final long MOCK_SEQUENCE_NUMBER = 1; + private ExoCastPlayer player; + private MediaItem.Builder itemBuilder; + private CastSessionManager.StateListener receiverAppStateListener; + private FakeClock clock; + @Mock private CastSessionManager sessionManager; + @Mock private SessionAvailabilityListener sessionAvailabilityListener; + @Mock private Player.EventListener playerEventListener; + + @Before + public void setUp() { + initMocks(this); + clock = new FakeClock(/* initialTimeMs= */ 0); + player = + new ExoCastPlayer( + listener -> { + receiverAppStateListener = listener; + return sessionManager; + }, + clock); + player.addListener(playerEventListener); + itemBuilder = new MediaItem.Builder(); + } + + @Test + public void exoCastPlayer_startsAndStopsSessionManager() { + // The session manager should have been started when setting up, with the creation of + // ExoCastPlayer. + verify(sessionManager).start(); + verifyNoMoreInteractions(sessionManager); + player.release(); + verify(sessionManager).stopTrackingSession(); + verifyNoMoreInteractions(sessionManager); + } + + @Test + public void exoCastPlayer_propagatesSessionStatus() { + player.setSessionAvailabilityListener(sessionAvailabilityListener); + verify(sessionAvailabilityListener, never()).onCastSessionAvailable(); + receiverAppStateListener.onCastSessionAvailable(); + verify(sessionAvailabilityListener).onCastSessionAvailable(); + verifyNoMoreInteractions(sessionAvailabilityListener); + receiverAppStateListener.onCastSessionUnavailable(); + verify(sessionAvailabilityListener).onCastSessionUnavailable(); + verifyNoMoreInteractions(sessionAvailabilityListener); + } + + @Test + public void addItemsToQueue_producesExpectedMessages() throws JSONException { + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); + MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); + MediaItem item5 = itemBuilder.setUuid(toUuid(5)).build(); + + player.addItemsToQueue(item1, item2); + assertMediaItemQueue(item1, item2); + + player.addItemsToQueue(1, item3, item4); + assertMediaItemQueue(item1, item3, item4, item2); + + player.addItemsToQueue(item5); + assertMediaItemQueue(item1, item3, item4, item2, item5); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); + verify(sessionManager, times(3)).send(messageCaptor.capture()); + assertMessageAddsItems( + /* message= */ messageCaptor.getAllValues().get(0), + /* index= */ C.INDEX_UNSET, + Arrays.asList(item1, item2)); + assertMessageAddsItems( + /* message= */ messageCaptor.getAllValues().get(1), + /* index= */ 1, + Arrays.asList(item3, item4)); + assertMessageAddsItems( + /* message= */ messageCaptor.getAllValues().get(2), + /* index= */ C.INDEX_UNSET, + Collections.singletonList(item5)); + } + + @Test + public void addItemsToQueue_masksRemoteUpdates() { + player.prepare(); + when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); + MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); + + player.addItemsToQueue(item1, item2); + assertMediaItemQueue(item1, item2); + + // Should be ignored due to a lower sequence number. + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) + .setItems(Arrays.asList(item3, item4)) + .build()); + + // Should override the current state. + assertMediaItemQueue(item1, item2); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) + .setItems(Arrays.asList(item3, item4)) + .build()); + + assertMediaItemQueue(item3, item4); + } + + @Test + public void addItemsToQueue_masksWindowIndexAsExpected() { + player.prepare(); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); + player.seekTo(/* windowIndex= */ 2, /* positionMs= */ 500); + + assertThat(player.getCurrentWindowIndex()).isEqualTo(2); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(2); + player.addItemsToQueue(/* optionalIndex= */ 0, itemBuilder.build()); + assertThat(player.getCurrentWindowIndex()).isEqualTo(3); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(3); + + player.addItemsToQueue(itemBuilder.build()); + assertThat(player.getCurrentWindowIndex()).isEqualTo(3); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(3); + } + + @Test + public void addItemsToQueue_doesNotAddDuplicateUuids() { + player.prepare(); + player.addItemsToQueue(itemBuilder.setUuid(toUuid(1)).build()); + assertThat(player.getQueueSize()).isEqualTo(1); + player.addItemsToQueue( + itemBuilder.setUuid(toUuid(1)).build(), itemBuilder.setUuid(toUuid(2)).build()); + assertThat(player.getQueueSize()).isEqualTo(2); + try { + player.addItemsToQueue( + itemBuilder.setUuid(toUuid(3)).build(), itemBuilder.setUuid(toUuid(3)).build()); + fail(); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void moveItemInQueue_behavesAsExpected() throws JSONException { + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); + player.addItemsToQueue(item1, item2, item3); + assertMediaItemQueue(item1, item2, item3); + player.moveItemInQueue(/* index= */ 0, /* newIndex= */ 2); + assertMediaItemQueue(item2, item3, item1); + player.moveItemInQueue(/* index= */ 1, /* newIndex= */ 1); + assertMediaItemQueue(item2, item3, item1); + player.moveItemInQueue(/* index= */ 1, /* newIndex= */ 0); + assertMediaItemQueue(item3, item2, item1); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); + verify(sessionManager, times(4)).send(messageCaptor.capture()); + // First sent message is an "add" message. + assertMessageMovesItem( + /* message= */ messageCaptor.getAllValues().get(1), item1, /* index= */ 2); + assertMessageMovesItem( + /* message= */ messageCaptor.getAllValues().get(2), item3, /* index= */ 1); + assertMessageMovesItem( + /* message= */ messageCaptor.getAllValues().get(3), item3, /* index= */ 0); + } + + @Test + public void moveItemInQueue_moveBeforeToAfter_masksWindowIndexAsExpected() { + player.prepare(); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); + player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); + + assertThat(player.getCurrentWindowIndex()).isEqualTo(1); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); + player.moveItemInQueue(/* index= */ 0, /* newIndex= */ 1); + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); + } + + @Test + public void moveItemInQueue_moveAfterToBefore_masksWindowIndexAsExpected() { + player.prepare(); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 500); + + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); + player.moveItemInQueue(/* index= */ 1, /* newIndex= */ 0); + assertThat(player.getCurrentWindowIndex()).isEqualTo(1); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); + } + + @Test + public void moveItemInQueue_moveCurrent_masksWindowIndexAsExpected() { + player.prepare(); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 500); + + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); + player.moveItemInQueue(/* index= */ 0, /* newIndex= */ 2); + assertThat(player.getCurrentWindowIndex()).isEqualTo(2); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(2); + } + + @Test + public void removeItemsFromQueue_masksMediaQueue() throws JSONException { + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); + MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); + MediaItem item5 = itemBuilder.setUuid(toUuid(5)).build(); + player.addItemsToQueue(item1, item2, item3, item4, item5); + assertMediaItemQueue(item1, item2, item3, item4, item5); + + player.removeItemFromQueue(2); + assertMediaItemQueue(item1, item2, item4, item5); + + player.removeRangeFromQueue(1, 3); + assertMediaItemQueue(item1, item5); + + player.clearQueue(); + assertMediaItemQueue(); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); + verify(sessionManager, times(4)).send(messageCaptor.capture()); + // First sent message is an "add" message. + assertMessageRemovesItems( + messageCaptor.getAllValues().get(1), Collections.singletonList(item3)); + assertMessageRemovesItems(messageCaptor.getAllValues().get(2), Arrays.asList(item2, item4)); + assertMessageRemovesItems(messageCaptor.getAllValues().get(3), Arrays.asList(item1, item5)); + } + + @Test + public void removeRangeFromQueue_beforeCurrentItem_masksWindowIndexAsExpected() { + player.prepare(); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); + player.seekTo(/* windowIndex= */ 2, /* positionMs= */ 500); + + assertThat(player.getCurrentWindowIndex()).isEqualTo(2); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(2); + player.removeRangeFromQueue(/* indexFrom= */ 0, /* indexExclusiveTo= */ 2); + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); + } + + @Test + public void removeRangeFromQueue_currentItem_masksWindowIndexAsExpected() { + player.prepare(); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); + player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); + + assertThat(player.getCurrentWindowIndex()).isEqualTo(1); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); + player.removeRangeFromQueue(/* indexFrom= */ 0, /* indexExclusiveTo= */ 2); + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); + } + + @Test + public void removeRangeFromQueue_currentItemWhichIsLast_transitionsToEnded() { + player.prepare(); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); + player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); + + assertThat(player.getCurrentWindowIndex()).isEqualTo(1); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); + player.removeRangeFromQueue(/* indexFrom= */ 1, /* indexExclusiveTo= */ 3); + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); + } + + @Test + public void clearQueue_resetsPlaybackPosition() { + player.prepare(); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); + player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); + + assertThat(player.getCurrentWindowIndex()).isEqualTo(1); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); + player.clearQueue(); + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); + } + + @Test + public void prepare_emptyQueue_transitionsToEnded() { + player.prepare(); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); + verify(playerEventListener).onPlayerStateChanged(/* playWhenReady=*/ false, Player.STATE_ENDED); + } + + @Test + public void prepare_withQueue_transitionsToBuffering() { + player.addItemsToQueue(itemBuilder.build()); + player.prepare(); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_BUFFERING); + verify(playerEventListener) + .onPlayerStateChanged(/* playWhenReady=*/ false, Player.STATE_BUFFERING); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Timeline.class); + verify(playerEventListener) + .onTimelineChanged( + argumentCaptor.capture(), + /* manifest= */ isNull(), + eq(Player.TIMELINE_CHANGE_REASON_PREPARED)); + assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(1); + } + + @Test + public void stop_withoutReset_leavesCurrentTimeline() { + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); + player.addItemsToQueue(itemBuilder.setUuid(toUuid(1)).build()); + player.prepare(); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_BUFFERING); + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET); + verify(playerEventListener) + .onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_BUFFERING); + verify(playerEventListener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + player.stop(/* reset= */ false); + verify(playerEventListener).onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_IDLE); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Timeline.class); + // Update for prepare. + verify(playerEventListener) + .onTimelineChanged( + argumentCaptor.capture(), + /* manifest= */ isNull(), + eq(Player.TIMELINE_CHANGE_REASON_PREPARED)); + assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(1); + + // Update for stop. + verifyNoMoreInteractions(playerEventListener); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(1); + } + + @Test + public void stop_withReset_clearsQueue() { + player.prepare(); + player.addItemsToQueue(itemBuilder.setUuid(toUuid(1)).build()); + verify(playerEventListener) + .onTimelineChanged( + any(Timeline.class), isNull(), eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET); + verify(playerEventListener) + .onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_BUFFERING); + player.stop(/* reset= */ true); + verify(playerEventListener).onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_IDLE); + + // Update for add. + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Timeline.class); + verify(playerEventListener) + .onTimelineChanged( + argumentCaptor.capture(), + /* manifest= */ isNull(), + eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); + assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(1); + + // Update for stop. + verify(playerEventListener) + .onTimelineChanged( + argumentCaptor.capture(), + /* manifest= */ isNull(), + eq(Player.TIMELINE_CHANGE_REASON_RESET)); + assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(0); + + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + } + + @Test + public void getCurrentTimeline_masksRemoteUpdates() { + player.prepare(); + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + player.addItemsToQueue(item1, item2); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Timeline.class); + verify(playerEventListener) + .onTimelineChanged( + messageCaptor.capture(), + /* manifest= */ isNull(), + eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); + Timeline reportedTimeline = messageCaptor.getValue(); + assertThat(reportedTimeline).isSameInstanceAs(player.getCurrentTimeline()); + assertThat(reportedTimeline.getWindowCount()).isEqualTo(2); + assertThat(reportedTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).durationUs) + .isEqualTo(C.TIME_UNSET); + assertThat(reportedTimeline.getWindow(/* windowIndex= */ 1, new Timeline.Window()).durationUs) + .isEqualTo(C.TIME_UNSET); + } + + @Test + public void getCurrentTimeline_exposesReceiverState() { + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) + .setPlaybackState(Player.STATE_BUFFERING) + .setItems(Arrays.asList(item1, item2)) + .setShuffleOrder(Arrays.asList(1, 0)) + .build()); + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Timeline.class); + verify(playerEventListener) + .onTimelineChanged( + messageCaptor.capture(), + /* manifest= */ isNull(), + eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); + Timeline reportedTimeline = messageCaptor.getValue(); + assertThat(reportedTimeline).isSameInstanceAs(player.getCurrentTimeline()); + assertThat(reportedTimeline.getWindowCount()).isEqualTo(2); + assertThat(reportedTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).durationUs) + .isEqualTo(C.TIME_UNSET); + assertThat(reportedTimeline.getWindow(/* windowIndex= */ 1, new Timeline.Window()).durationUs) + .isEqualTo(C.TIME_UNSET); + } + + @Test + public void timelineUpdateFromReceiver_matchesLocalState_doesNotCallEventLsitener() { + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); + MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); + + MediaItemInfo.Period period1 = + new MediaItemInfo.Period("id1", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 0); + MediaItemInfo.Period period2 = + new MediaItemInfo.Period( + "id2", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 1000000L); + MediaItemInfo.Period period3 = + new MediaItemInfo.Period( + "id3", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 2000000L); + HashMap mediaItemInfoMap1 = new HashMap<>(); + mediaItemInfoMap1.put( + toUuid(1), + new MediaItemInfo( + /* windowDurationUs= */ 3000L, + /* defaultStartPositionUs= */ 10, + /* periods= */ Arrays.asList(period1, period2, period3), + /* positionInFirstPeriodUs= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false)); + mediaItemInfoMap1.put( + toUuid(3), + new MediaItemInfo( + /* windowDurationUs= */ 2000L, + /* defaultStartPositionUs= */ 10, + /* periods= */ Arrays.asList(period1, period2), + /* positionInFirstPeriodUs= */ 500, + /* isSeekable= */ true, + /* isDynamic= */ false)); + + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(1) + .setPlaybackState(Player.STATE_BUFFERING) + .setItems(Arrays.asList(item1, item2, item3, item4)) + .setShuffleOrder(Arrays.asList(1, 0, 2, 3)) + .setMediaItemsInformation(mediaItemInfoMap1) + .build()); + verify(playerEventListener) + .onTimelineChanged( + any(), /* manifest= */ isNull(), eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); + verify(playerEventListener) + .onPlayerStateChanged( + /* playWhenReady= */ false, /* playbackState= */ Player.STATE_BUFFERING); + + HashMap mediaItemInfoMap2 = new HashMap<>(mediaItemInfoMap1); + mediaItemInfoMap2.put( + toUuid(5), + new MediaItemInfo( + /* windowDurationUs= */ 5, + /* defaultStartPositionUs= */ 0, + /* periods= */ Arrays.asList(period1, period2), + /* positionInFirstPeriodUs= */ 500, + /* isSeekable= */ true, + /* isDynamic= */ false)); + + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(1).setMediaItemsInformation(mediaItemInfoMap2).build()); + verifyNoMoreInteractions(playerEventListener); + } + + @Test + public void getPeriodIndex_producesExpectedOutput() { + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); + MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); + + MediaItemInfo.Period period1 = + new MediaItemInfo.Period("id1", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 0); + MediaItemInfo.Period period2 = + new MediaItemInfo.Period( + "id2", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 1000000L); + MediaItemInfo.Period period3 = + new MediaItemInfo.Period( + "id3", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 2000000L); + HashMap mediaItemInfoMap = new HashMap<>(); + mediaItemInfoMap.put( + toUuid(1), + new MediaItemInfo( + /* windowDurationUs= */ 3000L, + /* defaultStartPositionUs= */ 10, + /* periods= */ Arrays.asList(period1, period2, period3), + /* positionInFirstPeriodUs= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false)); + mediaItemInfoMap.put( + toUuid(3), + new MediaItemInfo( + /* windowDurationUs= */ 2000L, + /* defaultStartPositionUs= */ 10, + /* periods= */ Arrays.asList(period1, period2), + /* positionInFirstPeriodUs= */ 500, + /* isSeekable= */ true, + /* isDynamic= */ false)); + + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1L) + .setPlaybackState(Player.STATE_BUFFERING) + .setItems(Arrays.asList(item1, item2, item3, item4)) + .setShuffleOrder(Arrays.asList(1, 0, 3, 2)) + .setMediaItemsInformation(mediaItemInfoMap) + .setPlaybackPosition( + /* currentPlayingItemUuid= */ item3.uuid, + /* currentPlayingPeriodId= */ "id2", + /* currentPlaybackPositionMs= */ 500L) + .build()); + + assertThat(player.getCurrentPeriodIndex()).isEqualTo(5); + player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0L); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(3); + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1500L); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); + } + + @Test + public void exoCastPlayer_propagatesPlayerStateFromReceiver() { + ReceiverAppStateUpdate.Builder builder = + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1); + + // The first idle state update should be discarded, since it matches the current state. + receiverAppStateListener.onStateUpdateFromReceiverApp( + builder.setPlaybackState(Player.STATE_IDLE).build()); + receiverAppStateListener.onStateUpdateFromReceiverApp( + builder.setPlaybackState(Player.STATE_BUFFERING).build()); + receiverAppStateListener.onStateUpdateFromReceiverApp( + builder.setPlaybackState(Player.STATE_READY).build()); + receiverAppStateListener.onStateUpdateFromReceiverApp( + builder.setPlaybackState(Player.STATE_ENDED).build()); + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Integer.class); + verify(playerEventListener, times(3)) + .onPlayerStateChanged(/* playWhenReady= */ eq(false), messageCaptor.capture()); + List states = messageCaptor.getAllValues(); + assertThat(states).hasSize(3); + assertThat(states) + .isEqualTo(Arrays.asList(Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED)); + } + + @Test + public void setPlayWhenReady_changedLocally_notifiesListeners() { + player.setPlayWhenReady(false); + verify(playerEventListener, never()).onPlayerStateChanged(false, Player.STATE_IDLE); + player.setPlayWhenReady(true); + verify(playerEventListener).onPlayerStateChanged(true, Player.STATE_IDLE); + player.setPlayWhenReady(false); + verify(playerEventListener).onPlayerStateChanged(false, Player.STATE_IDLE); + } + + @Test + public void setPlayWhenReady_changedRemotely_notifiesListeners() { + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0).setPlayWhenReady(true).build()); + verify(playerEventListener) + .onPlayerStateChanged(/* playWhenReady= */ true, /* playbackState= */ Player.STATE_IDLE); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0).setPlayWhenReady(true).build()); + verifyNoMoreInteractions(playerEventListener); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0).setPlayWhenReady(false).build()); + verify(playerEventListener) + .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); + verifyNoMoreInteractions(playerEventListener); + } + + @Test + public void getPlayWhenReady_masksRemoteUpdates() { + when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); + player.setPlayWhenReady(true); + verify(playerEventListener) + .onPlayerStateChanged(/* playWhenReady= */ true, /* playbackState= */ Player.STATE_IDLE); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2).setPlayWhenReady(false).build()); + verifyNoMoreInteractions(playerEventListener); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3).setPlayWhenReady(true).build()); + verifyNoMoreInteractions(playerEventListener); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3).setPlayWhenReady(false).build()); + verify(playerEventListener) + .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); + } + + @Test + public void setRepeatMode_changedLocally_notifiesListeners() { + player.setRepeatMode(Player.REPEAT_MODE_OFF); + verifyNoMoreInteractions(playerEventListener); + player.setRepeatMode(Player.REPEAT_MODE_ONE); + verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); + player.setRepeatMode(Player.REPEAT_MODE_ONE); + verifyNoMoreInteractions(playerEventListener); + } + + @Test + public void setRepeatMode_changedRemotely_notifiesListeners() { + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0) + .setRepeatMode(Player.REPEAT_MODE_ONE) + .build()); + verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE); + } + + @Test + public void getRepeatMode_masksRemoteUpdates() { + when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); + player.setRepeatMode(Player.REPEAT_MODE_ALL); + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); + verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) + .setRepeatMode(Player.REPEAT_MODE_ONE) + .build()); + verifyNoMoreInteractions(playerEventListener); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) + .setRepeatMode(Player.REPEAT_MODE_ONE) + .build()); + verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); + } + + @Test + public void getPlaybackPosition_withStateChanges_producesExpectedOutput() { + UUID uuid = toUuid(1); + HashMap mediaItemInfoMap = new HashMap<>(); + + MediaItemInfo.Period period1 = new MediaItemInfo.Period("id1", 1000L, 0); + MediaItemInfo.Period period2 = new MediaItemInfo.Period("id2", 1000L, 0); + MediaItemInfo.Period period3 = new MediaItemInfo.Period("id3", 1000L, 0); + mediaItemInfoMap.put( + uuid, + new MediaItemInfo( + /* windowDurationUs= */ 1000L, + /* defaultStartPositionUs= */ 10, + /* periods= */ Arrays.asList(period1, period2, period3), + /* positionInFirstPeriodUs= */ 500, + /* isSeekable= */ true, + /* isDynamic= */ false)); + + when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(1L); + player.addItemsToQueue(itemBuilder.setUuid(uuid).build()); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) + .setPlaybackState(Player.STATE_BUFFERING) + .setMediaItemsInformation(mediaItemInfoMap) + .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1000L) + .build()); + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(1000L); + clock.advanceTime(/* timeDiffMs= */ 1L); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) + .setPlaybackState(Player.STATE_READY) + .build()); + // Play when ready is still false, so position should not change. + assertThat(player.getCurrentPosition()).isEqualTo(1000L); + player.setPlayWhenReady(true); + clock.advanceTime(1); + assertThat(player.getCurrentPosition()).isEqualTo(1001L); + clock.advanceTime(1); + assertThat(player.getCurrentPosition()).isEqualTo(1002L); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) + .setPlaybackState(Player.STATE_BUFFERING) + .setMediaItemsInformation(mediaItemInfoMap) + .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1010L) + .build()); + clock.advanceTime(1); + assertThat(player.getCurrentPosition()).isEqualTo(1010L); + clock.advanceTime(1); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) + .setPlaybackState(Player.STATE_READY) + .setMediaItemsInformation(mediaItemInfoMap) + .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1011L) + .build()); + clock.advanceTime(10); + assertThat(player.getCurrentPosition()).isEqualTo(1021L); + } + + @Test + public void getPlaybackPosition_withNonDefaultPlaybackSpeed_producesExpectedOutput() { + MediaItem item = itemBuilder.setUuid(toUuid(1)).build(); + MediaItemInfo info = + new MediaItemInfo( + /* windowDurationUs= */ 10000000, + /* defaultStartPositionUs= */ 3000000, + /* periods= */ Collections.singletonList( + new MediaItemInfo.Period( + /* id= */ "id", /* durationUs= */ 10000000, /* positionInWindowUs= */ 0)), + /* positionInFirstPeriodUs= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) + .setMediaItemsInformation(Collections.singletonMap(toUuid(1), info)) + .setShuffleOrder(Collections.singletonList(0)) + .setItems(Collections.singletonList(item)) + .setPlaybackPosition( + toUuid(1), /* currentPlayingPeriodId= */ "id", /* currentPlaybackPositionMs= */ 20L) + .setPlaybackState(Player.STATE_READY) + .setPlayWhenReady(true) + .build()); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(20); + clock.advanceTime(10); + assertThat(player.getCurrentPosition()).isEqualTo(30); + clock.advanceTime(10); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(1) + .setPlaybackPosition( + toUuid(1), /* currentPlayingPeriodId= */ "id", /* currentPlaybackPositionMs= */ 40L) + .setPlaybackParameters(new PlaybackParameters(2)) + .build()); + clock.advanceTime(10); + assertThat(player.getCurrentPosition()).isEqualTo(60); + } + + @Test + public void positionChanges_notifiesDiscontinuities() { + UUID uuid = toUuid(1); + HashMap mediaItemInfoMap = new HashMap<>(); + + MediaItemInfo.Period period1 = new MediaItemInfo.Period("id1", 1000L, 0); + MediaItemInfo.Period period2 = new MediaItemInfo.Period("id2", 1000L, 0); + MediaItemInfo.Period period3 = new MediaItemInfo.Period("id3", 1000L, 0); + mediaItemInfoMap.put( + uuid, + new MediaItemInfo( + /* windowDurationUs= */ 1000L, + /* defaultStartPositionUs= */ 10, + /* periods= */ Arrays.asList(period1, period2, period3), + /* positionInFirstPeriodUs= */ 500, + /* isSeekable= */ true, + /* isDynamic= */ false)); + + player.addItemsToQueue(itemBuilder.setUuid(uuid).build()); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) + .setPlaybackState(Player.STATE_BUFFERING) + .setMediaItemsInformation(mediaItemInfoMap) + .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1000L) + .setDiscontinuityReason(Player.DISCONTINUITY_REASON_SEEK) + .build()); + verify(playerEventListener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 999); + verify(playerEventListener, times(2)).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + } + + @Test + public void setShuffleModeEnabled_changedLocally_notifiesListeners() { + player.setShuffleModeEnabled(true); + verify(playerEventListener).onShuffleModeEnabledChanged(true); + player.setShuffleModeEnabled(true); + verifyNoMoreInteractions(playerEventListener); + } + + @Test + public void setShuffleModeEnabled_changedRemotely_notifiesListeners() { + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0) + .setShuffleModeEnabled(true) + .build()); + verify(playerEventListener).onShuffleModeEnabledChanged(true); + assertThat(player.getShuffleModeEnabled()).isTrue(); + } + + @Test + public void getShuffleMode_masksRemoteUpdates() { + when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); + player.setShuffleModeEnabled(true); + assertThat(player.getShuffleModeEnabled()).isTrue(); + verify(playerEventListener).onShuffleModeEnabledChanged(true); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) + .setShuffleModeEnabled(false) + .build()); + verifyNoMoreInteractions(playerEventListener); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) + .setShuffleModeEnabled(false) + .build()); + verify(playerEventListener).onShuffleModeEnabledChanged(false); + assertThat(player.getShuffleModeEnabled()).isFalse(); + } + + @Test + public void seekTo_inIdle_doesNotChangePlaybackState() { + player.prepare(); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build()); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 0); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_BUFFERING); + player.stop(false); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 0); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); + } + + @Test + public void seekTo_withTwoItems_producesExpectedMessage() { + player.prepare(); + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + player.addItemsToQueue(item1, item2); + player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1000); + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); + verify(sessionManager, times(3)).send(messageCaptor.capture()); + // Messages should be prepare, add and seek. + ExoCastMessage.SeekTo seekToMessage = + (ExoCastMessage.SeekTo) messageCaptor.getAllValues().get(2); + assertThat(seekToMessage.positionMs).isEqualTo(1000); + assertThat(seekToMessage.uuid).isEqualTo(toUuid(2)); + } + + @Test + public void seekTo_masksRemoteUpdates() { + player.prepare(); + when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + player.addItemsToQueue(item1, item2); + player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1000L); + verify(playerEventListener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(playerEventListener) + .onPlayerStateChanged( + /* playWhenReady= */ false, /* playbackState= */ Player.STATE_BUFFERING); + assertThat(player.getCurrentWindowIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(1000); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) + .setPlaybackPosition(toUuid(1), "id", 500L) + .build()); + assertThat(player.getCurrentWindowIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(1000); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) + .setPlaybackPosition(toUuid(1), "id", 500L) + .setDiscontinuityReason(Player.DISCONTINUITY_REASON_SEEK) + .build()); + verify(playerEventListener, times(2)).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(500); + } + + @Test + public void setPlaybackParameters_producesExpectedMessage() { + PlaybackParameters playbackParameters = + new PlaybackParameters(/* speed= */ .5f, /* pitch= */ .25f, /* skipSilence= */ true); + player.setPlaybackParameters(playbackParameters); + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); + verify(sessionManager).send(messageCaptor.capture()); + ExoCastMessage.SetPlaybackParameters message = + (ExoCastMessage.SetPlaybackParameters) messageCaptor.getValue(); + assertThat(message.playbackParameters).isEqualTo(playbackParameters); + } + + @Test + public void getTrackSelectionParameters_doesNotOverrideUnexpectedFields() { + when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); + DefaultTrackSelector.Parameters parameters = + DefaultTrackSelector.Parameters.DEFAULT + .buildUpon() + .setPreferredAudioLanguage("spa") + .setMaxVideoSize(/* maxVideoWidth= */ 3, /* maxVideoHeight= */ 3) + .build(); + player.setTrackSelectionParameters(parameters); + TrackSelectionParameters returned = + TrackSelectionParameters.DEFAULT.buildUpon().setPreferredAudioLanguage("deu").build(); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) + .setTrackSelectionParameters(returned) + .build()); + DefaultTrackSelector.Parameters result = + (DefaultTrackSelector.Parameters) player.getTrackSelectionParameters(); + assertThat(result.preferredAudioLanguage).isEqualTo("deu"); + assertThat(result.maxVideoHeight).isEqualTo(3); + assertThat(result.maxVideoWidth).isEqualTo(3); + } + + @Test + public void testExoCast_getRendererType() { + assertThat(player.getRendererCount()).isEqualTo(4); + assertThat(player.getRendererType(/* index= */ 0)).isEqualTo(C.TRACK_TYPE_VIDEO); + assertThat(player.getRendererType(/* index= */ 1)).isEqualTo(C.TRACK_TYPE_AUDIO); + assertThat(player.getRendererType(/* index= */ 2)).isEqualTo(C.TRACK_TYPE_TEXT); + assertThat(player.getRendererType(/* index= */ 3)).isEqualTo(C.TRACK_TYPE_METADATA); + } + + private static UUID toUuid(long lowerBits) { + return new UUID(0, lowerBits); + } + + private void assertMediaItemQueue(MediaItem... mediaItemQueue) { + assertThat(player.getQueueSize()).isEqualTo(mediaItemQueue.length); + for (int i = 0; i < mediaItemQueue.length; i++) { + assertThat(player.getQueueItem(i).uuid).isEqualTo(mediaItemQueue[i].uuid); + } + } + + private static void assertMessageAddsItems( + ExoCastMessage message, int index, List mediaItems) throws JSONException { + assertThat(message.method).isEqualTo(ExoCastConstants.METHOD_ADD_ITEMS); + JSONObject args = + new JSONObject(message.toJsonString(MOCK_SEQUENCE_NUMBER)).getJSONObject(KEY_ARGS); + if (index != C.INDEX_UNSET) { + assertThat(args.getInt(KEY_INDEX)).isEqualTo(index); + } else { + assertThat(args.has(KEY_INDEX)).isFalse(); + } + JSONArray itemsAsJson = args.getJSONArray(KEY_ITEMS); + assertThat(ReceiverAppStateUpdate.toMediaItemArrayList(itemsAsJson)).isEqualTo(mediaItems); + } + + private static void assertMessageMovesItem(ExoCastMessage message, MediaItem item, int index) + throws JSONException { + assertThat(message.method).isEqualTo(ExoCastConstants.METHOD_MOVE_ITEM); + JSONObject args = + new JSONObject(message.toJsonString(MOCK_SEQUENCE_NUMBER)).getJSONObject(KEY_ARGS); + assertThat(args.getString(KEY_UUID)).isEqualTo(item.uuid.toString()); + assertThat(args.getInt(KEY_INDEX)).isEqualTo(index); + } + + private static void assertMessageRemovesItems(ExoCastMessage message, List items) + throws JSONException { + assertThat(message.method).isEqualTo(ExoCastConstants.METHOD_REMOVE_ITEMS); + JSONObject args = + new JSONObject(message.toJsonString(MOCK_SEQUENCE_NUMBER)).getJSONObject(KEY_ARGS); + JSONArray uuidsAsJson = args.getJSONArray(KEY_UUIDS); + for (int i = 0; i < uuidsAsJson.length(); i++) { + assertThat(uuidsAsJson.getString(i)).isEqualTo(items.get(i).uuid.toString()); + } + } +} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastTimelineTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastTimelineTest.java new file mode 100644 index 0000000000..f6084339e4 --- /dev/null +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastTimelineTest.java @@ -0,0 +1,466 @@ +/* + * 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.ext.cast; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.UUID; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link ExoCastTimeline}. */ +@RunWith(AndroidJUnit4.class) +public class ExoCastTimelineTest { + + private MediaItem mediaItem1; + private MediaItem mediaItem2; + private MediaItem mediaItem3; + private MediaItem mediaItem4; + private MediaItem mediaItem5; + + @Before + public void setUp() { + MediaItem.Builder builder = new MediaItem.Builder(); + mediaItem1 = builder.setUuid(asUUID(1)).build(); + mediaItem2 = builder.setUuid(asUUID(2)).build(); + mediaItem3 = builder.setUuid(asUUID(3)).build(); + mediaItem4 = builder.setUuid(asUUID(4)).build(); + mediaItem5 = builder.setUuid(asUUID(5)).build(); + } + + @Test + public void getWindowCount_withNoItems_producesExpectedCount() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Collections.emptyList(), Collections.emptyMap(), new DefaultShuffleOrder(0)); + + assertThat(timeline.getWindowCount()).isEqualTo(0); + } + + @Test + public void getWindowCount_withFiveItems_producesExpectedCount() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(5)); + + assertThat(timeline.getWindowCount()).isEqualTo(5); + } + + @Test + public void getWindow_withNoMediaItemInfo_returnsEmptyWindow() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(5)); + Timeline.Window window = timeline.getWindow(2, new Timeline.Window(), /* setTag= */ true); + + assertThat(window.tag).isNull(); + assertThat(window.presentationStartTimeMs).isEqualTo(C.TIME_UNSET); + assertThat(window.windowStartTimeMs).isEqualTo(C.TIME_UNSET); + assertThat(window.isSeekable).isFalse(); + assertThat(window.isDynamic).isTrue(); + assertThat(window.defaultPositionUs).isEqualTo(0L); + assertThat(window.durationUs).isEqualTo(C.TIME_UNSET); + assertThat(window.firstPeriodIndex).isEqualTo(2); + assertThat(window.lastPeriodIndex).isEqualTo(2); + assertThat(window.positionInFirstPeriodUs).isEqualTo(0L); + } + + @Test + public void getWindow_withMediaItemInfo_returnsPopulatedWindow() { + MediaItem populatedMediaItem = new MediaItem.Builder().setAttachment("attachment").build(); + HashMap mediaItemInfos = new HashMap<>(); + MediaItemInfo.Period period1 = + new MediaItemInfo.Period("id1", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 0L); + MediaItemInfo.Period period2 = + new MediaItemInfo.Period( + "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L); + mediaItemInfos.put( + populatedMediaItem.uuid, + new MediaItemInfo( + /* windowDurationUs= */ 4000000L, + /* defaultStartPositionUs= */ 20L, + Arrays.asList(period1, period2), + /* positionInFirstPeriodUs= */ 500L, + /* isSeekable= */ true, + /* isDynamic= */ false)); + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, populatedMediaItem), + mediaItemInfos, + new DefaultShuffleOrder(5)); + Timeline.Window window = timeline.getWindow(4, new Timeline.Window(), /* setTag= */ true); + + assertThat(window.tag).isSameInstanceAs(populatedMediaItem.attachment); + assertThat(window.presentationStartTimeMs).isEqualTo(C.TIME_UNSET); + assertThat(window.windowStartTimeMs).isEqualTo(C.TIME_UNSET); + assertThat(window.isSeekable).isTrue(); + assertThat(window.isDynamic).isFalse(); + assertThat(window.defaultPositionUs).isEqualTo(20L); + assertThat(window.durationUs).isEqualTo(4000000L); + assertThat(window.firstPeriodIndex).isEqualTo(4); + assertThat(window.lastPeriodIndex).isEqualTo(5); + assertThat(window.positionInFirstPeriodUs).isEqualTo(500L); + } + + @Test + public void getPeriodCount_producesExpectedOutput() { + HashMap mediaItemInfos = new HashMap<>(); + MediaItemInfo.Period period1 = + new MediaItemInfo.Period( + "id1", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L); + MediaItemInfo.Period period2 = + new MediaItemInfo.Period( + "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 6000000L); + mediaItemInfos.put( + asUUID(2), + new MediaItemInfo( + /* windowDurationUs= */ 7000000L, + /* defaultStartPositionUs= */ 20L, + Arrays.asList(period1, period2), + /* positionInFirstPeriodUs= */ 0L, + /* isSeekable= */ true, + /* isDynamic= */ false)); + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + mediaItemInfos, + new DefaultShuffleOrder(5)); + + assertThat(timeline.getPeriodCount()).isEqualTo(6); + } + + @Test + public void getPeriod_forPopulatedPeriod_producesExpectedOutput() { + HashMap mediaItemInfos = new HashMap<>(); + MediaItemInfo.Period period1 = + new MediaItemInfo.Period( + "id1", /* durationUs= */ 4000000L, /* positionInWindowUs= */ 1000000L); + MediaItemInfo.Period period2 = + new MediaItemInfo.Period( + "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 6000000L); + mediaItemInfos.put( + asUUID(5), + new MediaItemInfo( + /* windowDurationUs= */ 7000000L, + /* defaultStartPositionUs= */ 20L, + Arrays.asList(period1, period2), + /* positionInFirstPeriodUs= */ 0L, + /* isSeekable= */ true, + /* isDynamic= */ false)); + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + mediaItemInfos, + new DefaultShuffleOrder(5)); + Timeline.Period period = + timeline.getPeriod(/* periodIndex= */ 5, new Timeline.Period(), /* setIds= */ true); + Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 5); + + assertThat(period.durationUs).isEqualTo(5000000L); + assertThat(period.windowIndex).isEqualTo(4); + assertThat(period.id).isEqualTo("id2"); + assertThat(period.uid).isEqualTo(periodUid); + } + + @Test + public void getPeriod_forEmptyPeriod_producesExpectedOutput() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(5)); + Timeline.Period period = timeline.getPeriod(2, new Timeline.Period(), /* setIds= */ true); + Object uid = timeline.getUidOfPeriod(/* periodIndex= */ 2); + + assertThat(period.durationUs).isEqualTo(C.TIME_UNSET); + assertThat(period.windowIndex).isEqualTo(2); + assertThat(period.id).isEqualTo(MediaItemInfo.EMPTY.periods.get(0).id); + assertThat(period.uid).isEqualTo(uid); + } + + @Test + public void getIndexOfPeriod_worksAcrossDifferentTimelines() { + MediaItemInfo.Period period1 = + new MediaItemInfo.Period( + "id1", /* durationUs= */ 4000000L, /* positionInWindowUs= */ 1000000L); + MediaItemInfo.Period period2 = + new MediaItemInfo.Period( + "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L); + + HashMap mediaItemInfos1 = new HashMap<>(); + mediaItemInfos1.put( + asUUID(1), + new MediaItemInfo( + /* windowDurationUs= */ 5000000L, + /* defaultStartPositionUs= */ 20L, + Collections.singletonList(period2), + /* positionInFirstPeriodUs= */ 0L, + /* isSeekable= */ true, + /* isDynamic= */ false)); + ExoCastTimeline timeline1 = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2), mediaItemInfos1, new DefaultShuffleOrder(2)); + + HashMap mediaItemInfos2 = new HashMap<>(); + mediaItemInfos2.put( + asUUID(1), + new MediaItemInfo( + /* windowDurationUs= */ 7000000L, + /* defaultStartPositionUs= */ 20L, + Arrays.asList(period1, period2), + /* positionInFirstPeriodUs= */ 0L, + /* isSeekable= */ true, + /* isDynamic= */ false)); + ExoCastTimeline timeline2 = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem2, mediaItem1, mediaItem3, mediaItem4, mediaItem5), + mediaItemInfos2, + new DefaultShuffleOrder(5)); + Object uidOfFirstPeriod = timeline1.getUidOfPeriod(0); + + assertThat(timeline1.getIndexOfPeriod(uidOfFirstPeriod)).isEqualTo(0); + assertThat(timeline2.getIndexOfPeriod(uidOfFirstPeriod)).isEqualTo(2); + } + + @Test + public void getIndexOfPeriod_forLastPeriod_producesExpectedOutput() { + MediaItemInfo.Period period1 = + new MediaItemInfo.Period( + "id1", /* durationUs= */ 4000000L, /* positionInWindowUs= */ 1000000L); + MediaItemInfo.Period period2 = + new MediaItemInfo.Period( + "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L); + + HashMap mediaItemInfos1 = new HashMap<>(); + mediaItemInfos1.put( + asUUID(5), + new MediaItemInfo( + /* windowDurationUs= */ 4000000L, + /* defaultStartPositionUs= */ 20L, + Collections.singletonList(period2), + /* positionInFirstPeriodUs= */ 0L, + /* isSeekable= */ true, + /* isDynamic= */ false)); + ExoCastTimeline singlePeriodTimeline = + ExoCastTimeline.createTimelineFor( + Collections.singletonList(mediaItem5), mediaItemInfos1, new DefaultShuffleOrder(1)); + Object periodUid = singlePeriodTimeline.getUidOfPeriod(0); + + HashMap mediaItemInfos2 = new HashMap<>(); + mediaItemInfos2.put( + asUUID(5), + new MediaItemInfo( + /* windowDurationUs= */ 7000000L, + /* defaultStartPositionUs= */ 20L, + Arrays.asList(period1, period2), + /* positionInFirstPeriodUs= */ 0L, + /* isSeekable= */ true, + /* isDynamic= */ false)); + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + mediaItemInfos2, + new DefaultShuffleOrder(5)); + + assertThat(timeline.getIndexOfPeriod(periodUid)).isEqualTo(5); + } + + @Test + public void getUidOfPeriod_withInvalidUid_returnsUnsetIndex() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(/* length= */ 5)); + + assertThat(timeline.getIndexOfPeriod(new Object())).isEqualTo(C.INDEX_UNSET); + } + + @Test + public void getFirstWindowIndex_returnsIndexAccordingToShuffleMode() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); + + assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(0); + assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(1); + } + + @Test + public void getLastWindowIndex_returnsIndexAccordingToShuffleMode() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); + + assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(4); + assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(3); + } + + @Test + public void getNextWindowIndex_repeatModeOne_returnsSameIndex() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(5)); + + for (int i = 0; i < 5; i++) { + assertThat( + timeline.getNextWindowIndex( + i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true)) + .isEqualTo(i); + assertThat( + timeline.getNextWindowIndex( + i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false)) + .isEqualTo(i); + } + } + + @Test + public void getNextWindowIndex_onLastIndex_returnsExpectedIndex() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); + + // Shuffle mode disabled: + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 4, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false)) + .isEqualTo(C.INDEX_UNSET); + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 4, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false)) + .isEqualTo(0); + // Shuffle mode enabled: + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 3, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) + .isEqualTo(C.INDEX_UNSET); + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 3, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true)) + .isEqualTo(1); + } + + @Test + public void getNextWindowIndex_inMiddleOfQueue_returnsNextIndex() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); + + // Shuffle mode disabled: + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 2, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false)) + .isEqualTo(3); + // Shuffle mode enabled: + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 2, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) + .isEqualTo(0); + } + + @Test + public void getPreviousWindowIndex_repeatModeOne_returnsSameIndex() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); + + for (int i = 0; i < 5; i++) { + assertThat( + timeline.getPreviousWindowIndex( + /* windowIndex= */ i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true)) + .isEqualTo(i); + assertThat( + timeline.getPreviousWindowIndex( + /* windowIndex= */ i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false)) + .isEqualTo(i); + } + } + + @Test + public void getPreviousWindowIndex_onFirstIndex_returnsExpectedIndex() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); + + // Shuffle mode disabled: + assertThat( + timeline.getPreviousWindowIndex( + /* windowIndex= */ 0, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false)) + .isEqualTo(C.INDEX_UNSET); + assertThat( + timeline.getPreviousWindowIndex( + /* windowIndex= */ 0, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false)) + .isEqualTo(4); + // Shuffle mode enabled: + assertThat( + timeline.getPreviousWindowIndex( + /* windowIndex= */ 1, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) + .isEqualTo(C.INDEX_UNSET); + assertThat( + timeline.getPreviousWindowIndex( + /* windowIndex= */ 1, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true)) + .isEqualTo(3); + } + + @Test + public void getPreviousWindowIndex_inMiddleOfQueue_returnsPreviousIndex() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); + + assertThat( + timeline.getPreviousWindowIndex( + /* windowIndex= */ 4, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false)) + .isEqualTo(3); + assertThat( + timeline.getPreviousWindowIndex( + /* windowIndex= */ 4, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) + .isEqualTo(0); + } + + private static UUID asUUID(long number) { + return new UUID(/* mostSigBits= */ 0L, number); + } +} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdateTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdateTest.java new file mode 100644 index 0000000000..fbe936a016 --- /dev/null +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdateTest.java @@ -0,0 +1,378 @@ +/* + * 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.ext.cast; + +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DEFAULT_START_POSITION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISCONTINUITY_REASON; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DURATION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ERROR_MESSAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_DYNAMIC; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_LOADING; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_SEEKABLE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_ITEMS_INFO; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_QUEUE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIODS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIOD_ID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_PARAMETERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_POSITION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_STATE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_IN_FIRST_PERIOD_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TRACK_SELECTION_PARAMETERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_WINDOW_DURATION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_SEEK; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_FORCED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_BUFFERING; +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.ShuffleOrder; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; +import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.UUID; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link ReceiverAppStateUpdate}. */ +@RunWith(AndroidJUnit4.class) +public class ReceiverAppStateUpdateTest { + + private static final long MOCK_SEQUENCE_NUMBER = 1; + + @Test + public void statusUpdate_withPlayWhenReady_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER).setPlayWhenReady(true).build(); + JSONObject stateMessage = createStateMessage().put(KEY_PLAY_WHEN_READY, true); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withPlaybackState_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) + .setPlaybackState(Player.STATE_BUFFERING) + .build(); + JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_STATE, STR_STATE_BUFFERING); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withMediaQueue_producesExpectedUpdate() throws JSONException { + HashMap requestHeaders = new HashMap<>(); + requestHeaders.put("key", "value"); + MediaItem.UriBundle media = new MediaItem.UriBundle(Uri.parse("www.media.com"), requestHeaders); + MediaItem.DrmScheme drmScheme1 = + new MediaItem.DrmScheme( + C.WIDEVINE_UUID, + new MediaItem.UriBundle(Uri.parse("www.widevine.com"), requestHeaders)); + MediaItem.DrmScheme drmScheme2 = + new MediaItem.DrmScheme( + C.PLAYREADY_UUID, + new MediaItem.UriBundle(Uri.parse("www.playready.com"), requestHeaders)); + MediaItem item = + new MediaItem.Builder() + .setTitle("title") + .setDescription("description") + .setMedia(media) + .setDrmSchemes(Arrays.asList(drmScheme1, drmScheme2)) + .setStartPositionUs(10) + .setEndPositionUs(20) + .build(); + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) + .setItems(Collections.singletonList(item)) + .build(); + JSONObject object = + createStateMessage() + .put(KEY_MEDIA_QUEUE, new JSONArray().put(ExoCastMessage.mediaItemAsJsonObject(item))); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(object.toString())).isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withRepeatMode_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) + .setRepeatMode(Player.REPEAT_MODE_OFF) + .build(); + JSONObject stateMessage = createStateMessage().put(KEY_REPEAT_MODE, STR_REPEAT_MODE_OFF); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withShuffleModeEnabled_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER).setShuffleModeEnabled(false).build(); + JSONObject stateMessage = createStateMessage().put(KEY_SHUFFLE_MODE_ENABLED, false); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withIsLoading_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER).setIsLoading(true).build(); + JSONObject stateMessage = createStateMessage().put(KEY_IS_LOADING, true); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withPlaybackParameters_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) + .setPlaybackParameters( + new PlaybackParameters( + /* speed= */ .5f, /* pitch= */ .25f, /* skipSilence= */ false)) + .build(); + JSONObject playbackParamsJson = + new JSONObject().put(KEY_SPEED, .5).put(KEY_PITCH, .25).put(KEY_SKIP_SILENCE, false); + JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_PARAMETERS, playbackParamsJson); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withTrackSelectionParameters_producesExpectedUpdate() + throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) + .setTrackSelectionParameters( + TrackSelectionParameters.DEFAULT + .buildUpon() + .setDisabledTextTrackSelectionFlags( + C.SELECTION_FLAG_FORCED | C.SELECTION_FLAG_DEFAULT) + .setPreferredAudioLanguage("esp") + .setPreferredTextLanguage("deu") + .setSelectUndeterminedTextLanguage(true) + .build()) + .build(); + + JSONArray selectionFlagsJson = + new JSONArray() + .put(ExoCastConstants.STR_SELECTION_FLAG_DEFAULT) + .put(STR_SELECTION_FLAG_FORCED); + JSONObject playbackParamsJson = + new JSONObject() + .put(KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS, selectionFlagsJson) + .put(KEY_PREFERRED_AUDIO_LANGUAGE, "esp") + .put(KEY_PREFERRED_TEXT_LANGUAGE, "deu") + .put(KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE, true); + JSONObject object = + createStateMessage().put(KEY_TRACK_SELECTION_PARAMETERS, playbackParamsJson); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(object.toString())).isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withError_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) + .setErrorMessage("error message") + .build(); + JSONObject stateMessage = createStateMessage().put(KEY_ERROR_MESSAGE, "error message"); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withPlaybackPosition_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) + .setPlaybackPosition( + new UUID(/* mostSigBits= */ 0, /* leastSigBits= */ 1), "period", 10L) + .build(); + JSONObject positionJson = + new JSONObject() + .put(KEY_UUID, new UUID(0, 1)) + .put(KEY_POSITION_MS, 10) + .put(KEY_PERIOD_ID, "period"); + JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_POSITION, positionJson); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withDiscontinuity_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) + .setPlaybackPosition( + new UUID(/* mostSigBits= */ 0, /* leastSigBits= */ 1), "period", 10L) + .setDiscontinuityReason(Player.DISCONTINUITY_REASON_SEEK) + .build(); + JSONObject positionJson = + new JSONObject() + .put(KEY_UUID, new UUID(0, 1)) + .put(KEY_POSITION_MS, 10) + .put(KEY_PERIOD_ID, "period") + .put(KEY_DISCONTINUITY_REASON, STR_DISCONTINUITY_REASON_SEEK); + JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_POSITION, positionJson); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withMediaItemInfo_producesExpectedTimeline() throws JSONException { + MediaItem.Builder builder = new MediaItem.Builder(); + MediaItem item1 = builder.setUuid(new UUID(0, 1)).build(); + MediaItem item2 = builder.setUuid(new UUID(0, 2)).build(); + + JSONArray periodsJson = new JSONArray(); + periodsJson + .put(new JSONObject().put(KEY_ID, "id1").put(KEY_DURATION_US, 5000000L)) + .put(new JSONObject().put(KEY_ID, "id2").put(KEY_DURATION_US, 7000000L)) + .put(new JSONObject().put(KEY_ID, "id3").put(KEY_DURATION_US, 6000000L)); + JSONObject mediaItemInfoForUuid1 = new JSONObject(); + mediaItemInfoForUuid1 + .put(KEY_WINDOW_DURATION_US, 10000000L) + .put(KEY_DEFAULT_START_POSITION_US, 1000000L) + .put(KEY_PERIODS, periodsJson) + .put(KEY_POSITION_IN_FIRST_PERIOD_US, 2000000L) + .put(KEY_IS_DYNAMIC, false) + .put(KEY_IS_SEEKABLE, true); + JSONObject mediaItemInfoMapJson = + new JSONObject().put(new UUID(0, 1).toString(), mediaItemInfoForUuid1); + + JSONObject receiverAppStateUpdateJson = + createStateMessage().put(KEY_MEDIA_ITEMS_INFO, mediaItemInfoMapJson); + ReceiverAppStateUpdate receiverAppStateUpdate = + ReceiverAppStateUpdate.fromJsonMessage(receiverAppStateUpdateJson.toString()); + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(item1, item2), + receiverAppStateUpdate.mediaItemsInformation, + new ShuffleOrder.DefaultShuffleOrder( + /* shuffledIndices= */ new int[] {1, 0}, /* randomSeed= */ 0)); + Timeline.Window window0 = + timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window(), /* setTag= */ true); + Timeline.Window window1 = + timeline.getWindow(/* windowIndex= */ 1, new Timeline.Window(), /* setTag= */ true); + Timeline.Period[] periods = new Timeline.Period[4]; + for (int i = 0; i < 4; i++) { + periods[i] = + timeline.getPeriod(/* periodIndex= */ i, new Timeline.Period(), /* setIds= */ true); + } + + assertThat(timeline.getWindowCount()).isEqualTo(2); + assertThat(window0.positionInFirstPeriodUs).isEqualTo(2000000L); + assertThat(window0.durationUs).isEqualTo(10000000L); + assertThat(window0.isDynamic).isFalse(); + assertThat(window0.isSeekable).isTrue(); + assertThat(window0.defaultPositionUs).isEqualTo(1000000L); + assertThat(window1.positionInFirstPeriodUs).isEqualTo(0L); + assertThat(window1.durationUs).isEqualTo(C.TIME_UNSET); + assertThat(window1.isDynamic).isTrue(); + assertThat(window1.isSeekable).isFalse(); + assertThat(window1.defaultPositionUs).isEqualTo(0L); + + assertThat(timeline.getPeriodCount()).isEqualTo(4); + assertThat(periods[0].id).isEqualTo("id1"); + assertThat(periods[0].getPositionInWindowUs()).isEqualTo(-2000000L); + assertThat(periods[0].durationUs).isEqualTo(5000000L); + assertThat(periods[1].id).isEqualTo("id2"); + assertThat(periods[1].durationUs).isEqualTo(7000000L); + assertThat(periods[1].getPositionInWindowUs()).isEqualTo(3000000L); + assertThat(periods[2].id).isEqualTo("id3"); + assertThat(periods[2].durationUs).isEqualTo(6000000L); + assertThat(periods[2].getPositionInWindowUs()).isEqualTo(10000000L); + assertThat(periods[3].durationUs).isEqualTo(C.TIME_UNSET); + } + + @Test + public void statusUpdate_withShuffleOrder_producesExpectedTimeline() throws JSONException { + MediaItem.Builder builder = new MediaItem.Builder(); + JSONObject receiverAppStateUpdateJson = + createStateMessage().put(KEY_SHUFFLE_ORDER, new JSONArray(Arrays.asList(2, 3, 1, 0))); + ReceiverAppStateUpdate receiverAppStateUpdate = + ReceiverAppStateUpdate.fromJsonMessage(receiverAppStateUpdateJson.toString()); + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + /* mediaItems= */ Arrays.asList( + builder.build(), builder.build(), builder.build(), builder.build()), + /* mediaItemInfoMap= */ Collections.emptyMap(), + /* shuffleOrder= */ new ShuffleOrder.DefaultShuffleOrder( + Util.toArray(receiverAppStateUpdate.shuffleOrder), /* randomSeed= */ 0)); + + assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(2); + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 2, + /* repeatMode= */ Player.REPEAT_MODE_OFF, + /* shuffleModeEnabled= */ true)) + .isEqualTo(3); + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 3, + /* repeatMode= */ Player.REPEAT_MODE_OFF, + /* shuffleModeEnabled= */ true)) + .isEqualTo(1); + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 1, + /* repeatMode= */ Player.REPEAT_MODE_OFF, + /* shuffleModeEnabled= */ true)) + .isEqualTo(0); + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 0, + /* repeatMode= */ Player.REPEAT_MODE_OFF, + /* shuffleModeEnabled= */ true)) + .isEqualTo(C.INDEX_UNSET); + } + + private static JSONObject createStateMessage() throws JSONException { + return new JSONObject().put(KEY_SEQUENCE_NUMBER, MOCK_SEQUENCE_NUMBER); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java index cb548ec3fd..12db27d68e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java @@ -71,6 +71,7 @@ public final class DecryptableSampleQueueReader { * @throws IOException The underlying error. */ public void maybeThrowError() throws IOException { + // TODO: Avoid throwing if the DRM error is not preventing a read operation. if (currentSession != null && currentSession.getState() == DrmSession.STATE_ERROR) { throw Assertions.checkNotNull(currentSession.getError()); } @@ -179,4 +180,21 @@ public final class DecryptableSampleQueueReader { previousSession.releaseReference(); } } + + /** Returns whether there is data available for reading. */ + public boolean isReady(boolean loadingFinished) { + @SampleQueue.PeekResult int nextInQueue = upstream.peekNext(); + if (nextInQueue == SampleQueue.PEEK_RESULT_NOTHING) { + return loadingFinished; + } else if (nextInQueue == SampleQueue.PEEK_RESULT_FORMAT) { + return true; + } else if (nextInQueue == SampleQueue.PEEK_RESULT_BUFFER_CLEAR) { + return currentSession == null || playClearSamplesWithoutKeys; + } else if (nextInQueue == SampleQueue.PEEK_RESULT_BUFFER_ENCRYPTED) { + return Assertions.checkNotNull(currentSession).getState() + == DrmSession.STATE_OPENED_WITH_KEYS; + } else { + throw new IllegalStateException(); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index b2c09bd70f..542565e70d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData; +import com.google.android.exoplayer2.source.SampleQueue.PeekResult; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -214,6 +215,27 @@ import com.google.android.exoplayer2.util.Util; readPosition = 0; } + /** + * Returns a {@link PeekResult} depending on what a following call to {@link #read + * read(formatHolder, decoderInputBuffer, formatRequired= false, allowOnlyClearBuffers= false, + * loadingFinished= false, decodeOnlyUntilUs= 0)} would result in. + */ + @SuppressWarnings("ReferenceEquality") + @PeekResult + public synchronized int peekNext(Format downstreamFormat) { + if (readPosition == length) { + return SampleQueue.PEEK_RESULT_NOTHING; + } + int relativeReadIndex = getRelativeIndex(readPosition); + if (formats[relativeReadIndex] != downstreamFormat) { + return SampleQueue.PEEK_RESULT_FORMAT; + } else { + return (flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0 + ? SampleQueue.PEEK_RESULT_BUFFER_ENCRYPTED + : SampleQueue.PEEK_RESULT_BUFFER_CLEAR; + } + } + /** * Attempts to read from the queue. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index 976a5d4e48..921afcdf2f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -28,6 +29,9 @@ import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.EOFException; import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; /** A queue of media samples. */ @@ -47,6 +51,27 @@ public class SampleQueue implements TrackOutput { } + /** Values returned by {@link #peekNext()}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + value = { + PEEK_RESULT_NOTHING, + PEEK_RESULT_FORMAT, + PEEK_RESULT_BUFFER_CLEAR, + PEEK_RESULT_BUFFER_ENCRYPTED + }) + @interface PeekResult {} + + /** Nothing is available for reading. */ + public static final int PEEK_RESULT_NOTHING = 0; + /** A format change is available for reading */ + public static final int PEEK_RESULT_FORMAT = 1; + /** A clear buffer is available for reading. */ + public static final int PEEK_RESULT_BUFFER_CLEAR = 2; + /** An encrypted buffer is available for reading. */ + public static final int PEEK_RESULT_BUFFER_ENCRYPTED = 3; + public static final int ADVANCE_FAILED = -1; private static final int INITIAL_SCRATCH_SIZE = 32; @@ -312,6 +337,16 @@ public class SampleQueue implements TrackOutput { return metadataQueue.setReadPosition(sampleIndex); } + /** + * Returns a {@link PeekResult} depending on what a following call to {@link #read + * read(formatHolder, decoderInputBuffer, formatRequired= false, allowOnlyClearBuffers= false, + * loadingFinished= false, decodeOnlyUntilUs= 0)} would result in. + */ + @PeekResult + public int peekNext() { + return metadataQueue.peekNext(downstreamFormat); + } + /** * Attempts to read from the queue. * From 883b3c8783468e98108581d3d0f284f514c81cde Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 24 Jun 2019 18:13:31 +0100 Subject: [PATCH 1390/1556] Update isMediaCodecException to return true for generic ISE on API 21+ if the stack trace contains MediaCodec. PiperOrigin-RevId: 254781909 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 ef3cb0bbe3..30083cb849 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 @@ -1743,8 +1743,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } private static boolean isMediaCodecException(IllegalStateException error) { - if (Util.SDK_INT >= 21) { - return isMediaCodecExceptionV21(error); + if (Util.SDK_INT >= 21 && isMediaCodecExceptionV21(error)) { + return true; } StackTraceElement[] stackTrace = error.getStackTrace(); return stackTrace.length > 0 && stackTrace[0].getClassName().equals("android.media.MediaCodec"); From 4e504bc485cfaef7b56deb7da6f7a33ce12b494b Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 26 Jun 2019 11:49:08 +0100 Subject: [PATCH 1391/1556] Rename DeferredMediaPeriod to MaskingMediaPeriod. This better reflects its usage and fits into our general naming pattern. PiperOrigin-RevId: 255157159 --- .../source/ConcatenatingMediaSource.java | 10 ++-- ...diaPeriod.java => MaskingMediaPeriod.java} | 12 ++--- .../exoplayer2/source/ads/AdsMediaSource.java | 46 +++++++++---------- 3 files changed, 34 insertions(+), 34 deletions(-) rename library/core/src/main/java/com/google/android/exoplayer2/source/{DeferredMediaPeriod.java => MaskingMediaPeriod.java} (94%) 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 e73fdd58a3..c031fcde21 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 @@ -456,8 +456,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource activeMediaPeriods; + public final List activeMediaPeriods; public DeferredTimeline timeline; public int childIndex; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java similarity index 94% rename from library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java index 95a218bfe7..344c4989eb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java @@ -32,7 +32,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * #createPeriod(MediaPeriodId)} has been called. This is useful if you need to return a media * period immediately but the media source that should create it is not yet prepared. */ -public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { +public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callback { /** Listener for preparation errors. */ public interface PrepareErrorListener { @@ -45,7 +45,7 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb /** The {@link MediaSource} which will create the actual media period. */ public final MediaSource mediaSource; - /** The {@link MediaPeriodId} used to create the deferred media period. */ + /** The {@link MediaPeriodId} used to create the masking media period. */ public final MediaPeriodId id; private final Allocator allocator; @@ -58,14 +58,14 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb private long preparePositionOverrideUs; /** - * Creates a new deferred media period. + * Creates a new masking media period. * * @param mediaSource The media source to wrap. - * @param id The identifier used to create the deferred media period. + * @param id The identifier used to create the masking media period. * @param allocator The allocator used to create the media period. * @param preparePositionUs The expected start position, in microseconds. */ - public DeferredMediaPeriod( + public MaskingMediaPeriod( MediaSource mediaSource, MediaPeriodId id, Allocator allocator, long preparePositionUs) { this.id = id; this.allocator = allocator; @@ -85,7 +85,7 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb this.listener = listener; } - /** Returns the position at which the deferred media period was prepared, in microseconds. */ + /** Returns the position at which the masking media period was prepared, in microseconds. */ public long getPreparePositionUs() { return preparePositionUs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 78b0f6de11..dd4c0d26b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -23,7 +23,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.CompositeMediaSource; -import com.google.android.exoplayer2.source.DeferredMediaPeriod; +import com.google.android.exoplayer2.source.MaskingMediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; @@ -128,7 +128,7 @@ public final class AdsMediaSource extends CompositeMediaSource { private final AdsLoader adsLoader; private final AdsLoader.AdViewProvider adViewProvider; private final Handler mainHandler; - private final Map> deferredMediaPeriodByAdMediaSource; + private final Map> maskingMediaPeriodByAdMediaSource; private final Timeline.Period period; // Accessed on the player thread. @@ -179,7 +179,7 @@ public final class AdsMediaSource extends CompositeMediaSource { this.adsLoader = adsLoader; this.adViewProvider = adViewProvider; mainHandler = new Handler(Looper.getMainLooper()); - deferredMediaPeriodByAdMediaSource = new HashMap<>(); + maskingMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; adGroupTimelines = new Timeline[0][]; @@ -219,29 +219,29 @@ public final class AdsMediaSource extends CompositeMediaSource { if (mediaSource == null) { mediaSource = adMediaSourceFactory.createMediaSource(adUri); adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = mediaSource; - deferredMediaPeriodByAdMediaSource.put(mediaSource, new ArrayList<>()); + maskingMediaPeriodByAdMediaSource.put(mediaSource, new ArrayList<>()); prepareChildSource(id, mediaSource); } - DeferredMediaPeriod deferredMediaPeriod = - new DeferredMediaPeriod(mediaSource, id, allocator, startPositionUs); - deferredMediaPeriod.setPrepareErrorListener( + MaskingMediaPeriod maskingMediaPeriod = + new MaskingMediaPeriod(mediaSource, id, allocator, startPositionUs); + maskingMediaPeriod.setPrepareErrorListener( new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup)); - List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); + List mediaPeriods = maskingMediaPeriodByAdMediaSource.get(mediaSource); if (mediaPeriods == null) { Object periodUid = Assertions.checkNotNull(adGroupTimelines[adGroupIndex][adIndexInAdGroup]) .getUidOfPeriod(/* periodIndex= */ 0); MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, id.windowSequenceNumber); - deferredMediaPeriod.createPeriod(adSourceMediaPeriodId); + maskingMediaPeriod.createPeriod(adSourceMediaPeriodId); } else { - // Keep track of the deferred media period so it can be populated with the real media period + // Keep track of the masking media period so it can be populated with the real media period // when the source's info becomes available. - mediaPeriods.add(deferredMediaPeriod); + mediaPeriods.add(maskingMediaPeriod); } - return deferredMediaPeriod; + return maskingMediaPeriod; } else { - DeferredMediaPeriod mediaPeriod = - new DeferredMediaPeriod(contentMediaSource, id, allocator, startPositionUs); + MaskingMediaPeriod mediaPeriod = + new MaskingMediaPeriod(contentMediaSource, id, allocator, startPositionUs); mediaPeriod.createPeriod(id); return mediaPeriod; } @@ -249,13 +249,13 @@ public final class AdsMediaSource extends CompositeMediaSource { @Override public void releasePeriod(MediaPeriod mediaPeriod) { - DeferredMediaPeriod deferredMediaPeriod = (DeferredMediaPeriod) mediaPeriod; - List mediaPeriods = - deferredMediaPeriodByAdMediaSource.get(deferredMediaPeriod.mediaSource); + MaskingMediaPeriod maskingMediaPeriod = (MaskingMediaPeriod) mediaPeriod; + List mediaPeriods = + maskingMediaPeriodByAdMediaSource.get(maskingMediaPeriod.mediaSource); if (mediaPeriods != null) { - mediaPeriods.remove(deferredMediaPeriod); + mediaPeriods.remove(maskingMediaPeriod); } - deferredMediaPeriod.releasePeriod(); + maskingMediaPeriod.releasePeriod(); } @Override @@ -263,7 +263,7 @@ public final class AdsMediaSource extends CompositeMediaSource { super.releaseSourceInternal(); Assertions.checkNotNull(componentListener).release(); componentListener = null; - deferredMediaPeriodByAdMediaSource.clear(); + maskingMediaPeriodByAdMediaSource.clear(); contentTimeline = null; contentManifest = null; adPlaybackState = null; @@ -319,11 +319,11 @@ public final class AdsMediaSource extends CompositeMediaSource { int adIndexInAdGroup, Timeline timeline) { Assertions.checkArgument(timeline.getPeriodCount() == 1); adGroupTimelines[adGroupIndex][adIndexInAdGroup] = timeline; - List mediaPeriods = deferredMediaPeriodByAdMediaSource.remove(mediaSource); + List mediaPeriods = maskingMediaPeriodByAdMediaSource.remove(mediaSource); if (mediaPeriods != null) { Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); for (int i = 0; i < mediaPeriods.size(); i++) { - DeferredMediaPeriod mediaPeriod = mediaPeriods.get(i); + MaskingMediaPeriod mediaPeriod = mediaPeriods.get(i); MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, mediaPeriod.id.windowSequenceNumber); mediaPeriod.createPeriod(adSourceMediaPeriodId); @@ -413,7 +413,7 @@ public final class AdsMediaSource extends CompositeMediaSource { } } - private final class AdPrepareErrorListener implements DeferredMediaPeriod.PrepareErrorListener { + private final class AdPrepareErrorListener implements MaskingMediaPeriod.PrepareErrorListener { private final Uri adUri; private final int adGroupIndex; From 40d44c48e59c31da8abec4492dbe96ba68152471 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 26 Jun 2019 15:16:22 +0100 Subject: [PATCH 1392/1556] Add threading model note to hello-word page Also add layer of indirection between code and the guide, to make moving content easier going forward. PiperOrigin-RevId: 255182216 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index da66f3dd10..1c6458c551 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -1233,8 +1233,7 @@ public class SimpleExoPlayer extends BasePlayer Log.w( TAG, "Player is accessed on the wrong thread. See " - + "https://exoplayer.dev/troubleshooting.html#" - + "what-do-player-is-accessed-on-the-wrong-thread-warnings-mean", + + "https://exoplayer.dev/issues/player-accessed-on-wrong-thread", hasNotifiedFullWrongThreadWarning ? null : new IllegalStateException()); hasNotifiedFullWrongThreadWarning = true; } From 8faac0344b4340fbd34e623620da15575a67a4f7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 27 Jun 2019 12:08:23 +0100 Subject: [PATCH 1393/1556] Fix checkerframework 2.8.2 warnings. The updated version issues more warnings than before. Most of the changes are related to annotation placement. PiperOrigin-RevId: 255371743 --- .../ext/cast/ReceiverAppStateUpdate.java | 28 +++++++++---------- .../exoplayer2/ext/gvr/GvrPlayerActivity.java | 12 ++++---- .../DefaultPlaybackSessionManager.java | 2 +- .../exoplayer2/analytics/PlaybackStats.java | 3 +- .../analytics/PlaybackStatsListener.java | 15 +++++----- .../source/DecryptableSampleQueueReader.java | 2 +- .../upstream/ByteArrayDataSink.java | 2 +- .../upstream/DefaultBandwidthMeter.java | 2 +- .../cache/CacheFileMetadataIndex.java | 2 +- .../upstream/cache/SimpleCache.java | 2 +- .../exoplayer2/ui/TrackSelectionView.java | 2 +- .../ui/spherical/CanvasRenderer.java | 4 +-- .../ui/spherical/ProjectionRenderer.java | 2 +- 13 files changed, 39 insertions(+), 39 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java index 8cb6056340..c1b12428d4 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java @@ -110,20 +110,20 @@ public final class ReceiverAppStateUpdate { public static final class Builder { private final long sequenceNumber; - @MonotonicNonNull private Boolean playWhenReady; - @MonotonicNonNull private Integer playbackState; - @MonotonicNonNull private List items; - @MonotonicNonNull private Integer repeatMode; - @MonotonicNonNull private Boolean shuffleModeEnabled; - @MonotonicNonNull private Boolean isLoading; - @MonotonicNonNull private PlaybackParameters playbackParameters; - @MonotonicNonNull private TrackSelectionParameters trackSelectionParameters; - @MonotonicNonNull private String errorMessage; - @MonotonicNonNull private Integer discontinuityReason; - @MonotonicNonNull private UUID currentPlayingItemUuid; - @MonotonicNonNull private String currentPlayingPeriodId; - @MonotonicNonNull private Long currentPlaybackPositionMs; - @MonotonicNonNull private List shuffleOrder; + private @MonotonicNonNull Boolean playWhenReady; + private @MonotonicNonNull Integer playbackState; + private @MonotonicNonNull List items; + private @MonotonicNonNull Integer repeatMode; + private @MonotonicNonNull Boolean shuffleModeEnabled; + private @MonotonicNonNull Boolean isLoading; + private @MonotonicNonNull PlaybackParameters playbackParameters; + private @MonotonicNonNull TrackSelectionParameters trackSelectionParameters; + private @MonotonicNonNull String errorMessage; + private @MonotonicNonNull Integer discontinuityReason; + private @MonotonicNonNull UUID currentPlayingItemUuid; + private @MonotonicNonNull String currentPlayingPeriodId; + private @MonotonicNonNull Long currentPlaybackPositionMs; + private @MonotonicNonNull List shuffleOrder; private Map mediaItemsInformation; private Builder(long sequenceNumber) { diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java index 38fa3a36e5..e22c97859a 100644 --- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java +++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java @@ -61,12 +61,12 @@ public abstract class GvrPlayerActivity extends GvrActivity { private final Handler mainHandler; @Nullable private Player player; - @MonotonicNonNull private GlViewGroup glView; - @MonotonicNonNull private ControllerManager controllerManager; - @MonotonicNonNull private SurfaceTexture surfaceTexture; - @MonotonicNonNull private Surface surface; - @MonotonicNonNull private SceneRenderer scene; - @MonotonicNonNull private PlayerControlView playerControl; + private @MonotonicNonNull GlViewGroup glView; + private @MonotonicNonNull ControllerManager controllerManager; + private @MonotonicNonNull SurfaceTexture surfaceTexture; + private @MonotonicNonNull Surface surface; + private @MonotonicNonNull SceneRenderer scene; + private @MonotonicNonNull PlayerControlView playerControl; public GvrPlayerActivity() { mainHandler = new Handler(Looper.getMainLooper()); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java index 4ac7ad6506..183a74544d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java @@ -46,7 +46,7 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag private final Timeline.Period period; private final HashMap sessions; - @MonotonicNonNull private Listener listener; + private @MonotonicNonNull Listener listener; private Timeline currentTimeline; @Nullable private MediaPeriodId currentMediaPeriodId; @Nullable private String activeSessionId; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java index f633bfbf8e..ed127bc550 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java @@ -502,8 +502,7 @@ public final class PlaybackStats { * @return The {@link PlaybackState} at that time, or {@link #PLAYBACK_STATE_NOT_STARTED} if the * given time is before the first known playback state in the history. */ - @PlaybackState - public int getPlaybackStateAtTime(long realtimeMs) { + public @PlaybackState int getPlaybackStateAtTime(long realtimeMs) { @PlaybackState int state = PLAYBACK_STATE_NOT_STARTED; for (Pair timeAndState : playbackStateHistory) { if (timeAndState.first.realtimeMs > realtimeMs) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java index 8c58133704..6444b4747f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -452,7 +452,7 @@ public final class PlaybackStatsListener private int nonFatalErrorCount; // Current player state tracking. - @PlaybackState private int currentPlaybackState; + private @PlaybackState int currentPlaybackState; private long currentPlaybackStateStartTimeMs; private boolean isSeeking; private boolean isForeground; @@ -713,8 +713,6 @@ public final class PlaybackStatsListener * * @param isFinal Whether this is the final build and no further events are expected. */ - // TODO(b/133387873): incompatible types in conditional expression. - @SuppressWarnings("nullness:conditional.type.incompatible") public PlaybackStats build(boolean isFinal) { long[] playbackStateDurationsMs = this.playbackStateDurationsMs; List mediaTimeHistory = this.mediaTimeHistory; @@ -739,6 +737,10 @@ public final class PlaybackStatsListener : playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND]; boolean hasBackgroundJoin = playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND] > 0; + List> videoHistory = + isFinal ? videoFormatHistory : new ArrayList<>(videoFormatHistory); + List> audioHistory = + isFinal ? audioFormatHistory : new ArrayList<>(audioFormatHistory); return new PlaybackStats( /* playbackCount= */ 1, playbackStateDurationsMs, @@ -757,8 +759,8 @@ public final class PlaybackStatsListener rebufferCount, maxRebufferTimeMs, /* adPlaybackCount= */ isAd ? 1 : 0, - isFinal ? videoFormatHistory : new ArrayList<>(videoFormatHistory), - isFinal ? audioFormatHistory : new ArrayList<>(audioFormatHistory), + videoHistory, + audioHistory, videoFormatHeightTimeMs, videoFormatHeightTimeProduct, videoFormatBitrateTimeMs, @@ -826,8 +828,7 @@ public final class PlaybackStatsListener } } - @PlaybackState - private int resolveNewPlaybackState() { + private @PlaybackState int resolveNewPlaybackState() { if (isSuspended) { // Keep VIDEO_STATE_ENDED if playback naturally ended (or progressed to next item). return currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java index 12db27d68e..4f0c5b87aa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java @@ -39,7 +39,7 @@ public final class DecryptableSampleQueueReader { private final DrmSessionManager sessionManager; private final FormatHolder formatHolder; private final boolean playClearSamplesWithoutKeys; - @MonotonicNonNull private Format currentFormat; + private @MonotonicNonNull Format currentFormat; @Nullable private DrmSession currentSession; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java index a9f9da0a95..2ba6ab4c69 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java @@ -29,7 +29,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ public final class ByteArrayDataSink implements DataSink { - @MonotonicNonNull private ByteArrayOutputStream stream; + private @MonotonicNonNull ByteArrayOutputStream stream; @Override public void open(DataSpec dataSpec) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java index b2333516a8..4145d9a1c7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java @@ -413,7 +413,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList */ private static class ConnectivityActionReceiver extends BroadcastReceiver { - @MonotonicNonNull private static ConnectivityActionReceiver staticInstance; + private static @MonotonicNonNull ConnectivityActionReceiver staticInstance; private final Handler mainHandler; private final ArrayList> bandwidthMeters; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java index 2a8b393ed3..2488ae0ff3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java @@ -59,7 +59,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final DatabaseProvider databaseProvider; - @MonotonicNonNull private String tableName; + private @MonotonicNonNull String tableName; /** * Deletes index data for the specified cache. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 1d4481b5cc..ea37612c88 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -75,7 +75,7 @@ public final class SimpleCache implements Cache { private long uid; private long totalSpace; private boolean released; - @MonotonicNonNull private CacheException initializationException; + private @MonotonicNonNull CacheException initializationException; /** * Returns whether {@code cacheFolder} is locked by a {@link SimpleCache} instance. To unlock the 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 c55cf31149..02ed0a534e 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 @@ -67,7 +67,7 @@ public class TrackSelectionView extends LinearLayout { private TrackNameProvider trackNameProvider; private CheckedTextView[][] trackViews; - @MonotonicNonNull private MappedTrackInfo mappedTrackInfo; + private @MonotonicNonNull MappedTrackInfo mappedTrackInfo; private int rendererIndex; private TrackGroupArray trackGroups; private boolean isDisabled; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java index 3d7e57bbd2..6ef9d4907d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java @@ -101,8 +101,8 @@ public final class CanvasRenderer { // GL initialization. The client of this class acquires a Canvas from the Surface, writes to it // and posts it. This marks the Surface as dirty. The GL code then updates the SurfaceTexture // when rendering only if it is dirty. - @MonotonicNonNull private SurfaceTexture displaySurfaceTexture; - @MonotonicNonNull private Surface displaySurface; + private @MonotonicNonNull SurfaceTexture displaySurfaceTexture; + private @MonotonicNonNull Surface displaySurface; public CanvasRenderer() { vertexBuffer = GlUtil.createBuffer(COORDS_PER_VERTEX * VERTEX_COUNT); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java index 8a211d0879..9a8c787e77 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java @@ -19,11 +19,11 @@ import static com.google.android.exoplayer2.util.GlUtil.checkGlError; import android.opengl.GLES11Ext; import android.opengl.GLES20; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.GlUtil; import com.google.android.exoplayer2.video.spherical.Projection; import java.nio.FloatBuffer; -import org.checkerframework.checker.nullness.qual.Nullable; /** * Utility class to render spherical meshes for video or images. Call {@link #init()} on the GL From 3c2afb16e63ea71155da39f5647aa227c6b36f26 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 27 Jun 2019 12:59:56 +0100 Subject: [PATCH 1394/1556] Cleanup: Remove deprecated ChunkSampleStream constructor PiperOrigin-RevId: 255377347 --- .../source/chunk/ChunkSampleStream.java | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 499aea6a0c..efc3b47596 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -26,7 +26,6 @@ import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; @@ -84,46 +83,6 @@ public class ChunkSampleStream implements SampleStream, S /* package */ long decodeOnlyUntilPositionUs; /* package */ boolean loadingFinished; - /** - * Constructs an instance. - * - * @param primaryTrackType The type of the primary track. One of the {@link C} {@code - * TRACK_TYPE_*} constants. - * @param embeddedTrackTypes The types of any embedded tracks, or null. - * @param embeddedTrackFormats The formats of the embedded tracks, or null. - * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. - * @param callback An {@link Callback} for the stream. - * @param allocator An {@link Allocator} from which allocations can be obtained. - * @param positionUs The position from which to start loading media. - * @param minLoadableRetryCount The minimum number of times that the source should retry a load - * before propagating an error. - * @param eventDispatcher A dispatcher to notify of events. - * @deprecated Use {@link #ChunkSampleStream(int, int[], Format[], ChunkSource, Callback, - * Allocator, long, LoadErrorHandlingPolicy, EventDispatcher)} instead. - */ - @Deprecated - public ChunkSampleStream( - int primaryTrackType, - @Nullable int[] embeddedTrackTypes, - @Nullable Format[] embeddedTrackFormats, - T chunkSource, - Callback> callback, - Allocator allocator, - long positionUs, - int minLoadableRetryCount, - EventDispatcher eventDispatcher) { - this( - primaryTrackType, - embeddedTrackTypes, - embeddedTrackFormats, - chunkSource, - callback, - allocator, - positionUs, - new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), - eventDispatcher); - } - /** * Constructs an instance. * From c974f74b1f626874c92dbe0ad4fdc6cbcc865eed Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 27 Jun 2019 13:04:32 +0100 Subject: [PATCH 1395/1556] Cleanup: Remove deprecated DataSpec.postBody PiperOrigin-RevId: 255378274 --- .../com/google/android/exoplayer2/upstream/DataSpec.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index 99a3d271bd..c2007b19a3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -95,13 +95,11 @@ public final class DataSpec { public final @HttpMethod int httpMethod; /** - * The HTTP body, null otherwise. If the body is non-null, then httpBody.length will be non-zero. + * The HTTP request body, null otherwise. If the body is non-null, then httpBody.length will be + * non-zero. */ @Nullable public final byte[] httpBody; - /** @deprecated Use {@link #httpBody} instead. */ - @Deprecated @Nullable public final byte[] postBody; - /** * The absolute position of the data in the full stream. */ @@ -251,7 +249,6 @@ public final class DataSpec { this.uri = uri; this.httpMethod = httpMethod; this.httpBody = (httpBody != null && httpBody.length != 0) ? httpBody : null; - this.postBody = this.httpBody; this.absoluteStreamPosition = absoluteStreamPosition; this.position = position; this.length = length; From 2a366e76b778a01e9c58844a3caaf90275b582d4 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 27 Jun 2019 13:14:00 +0100 Subject: [PATCH 1396/1556] Cleanup: Remove deprecated message sending functionality PiperOrigin-RevId: 255379393 --- .../google/android/exoplayer2/ExoPlayer.java | 37 ----------------- .../android/exoplayer2/ExoPlayerImpl.java | 41 ------------------- .../android/exoplayer2/SimpleExoPlayer.java | 14 ------- .../exoplayer2/testutil/StubExoPlayer.java | 14 ------- 4 files changed, 106 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 d0f9e2ae04..ee29af9c99 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 @@ -117,30 +117,6 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; */ public interface ExoPlayer extends Player { - /** @deprecated Use {@link PlayerMessage.Target} instead. */ - @Deprecated - interface ExoPlayerComponent extends PlayerMessage.Target {} - - /** @deprecated Use {@link PlayerMessage} instead. */ - @Deprecated - final class ExoPlayerMessage { - - /** The target to receive the message. */ - public final PlayerMessage.Target target; - /** The type of the message. */ - public final int messageType; - /** The message. */ - public final Object message; - - /** @deprecated Use {@link ExoPlayer#createMessage(PlayerMessage.Target)} instead. */ - @Deprecated - public ExoPlayerMessage(PlayerMessage.Target target, int messageType, Object message) { - this.target = target; - this.messageType = messageType; - this.message = message; - } - } - /** Returns the {@link Looper} associated with the playback thread. */ Looper getPlaybackLooper(); @@ -181,19 +157,6 @@ public interface ExoPlayer extends Player { */ PlayerMessage createMessage(PlayerMessage.Target target); - /** @deprecated Use {@link #createMessage(PlayerMessage.Target)} instead. */ - @Deprecated - @SuppressWarnings("deprecation") - void sendMessages(ExoPlayerMessage... messages); - - /** - * @deprecated Use {@link #createMessage(PlayerMessage.Target)} with {@link - * PlayerMessage#blockUntilDelivered()}. - */ - @Deprecated - @SuppressWarnings("deprecation") - void blockingSendMessages(ExoPlayerMessage... messages); - /** * Sets the parameters that control how seek operations are performed. * 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 c458f22050..945bd32d30 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 @@ -35,8 +35,6 @@ import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; /** An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayerFactory}. */ @@ -410,15 +408,6 @@ import java.util.concurrent.CopyOnWriteArrayList; /* playbackState= */ Player.STATE_IDLE); } - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void sendMessages(ExoPlayerMessage... messages) { - for (ExoPlayerMessage message : messages) { - createMessage(message.target).setType(message.messageType).setPayload(message.message).send(); - } - } - @Override public PlayerMessage createMessage(Target target) { return new PlayerMessage( @@ -429,36 +418,6 @@ import java.util.concurrent.CopyOnWriteArrayList; internalPlayerHandler); } - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void blockingSendMessages(ExoPlayerMessage... messages) { - List playerMessages = new ArrayList<>(); - for (ExoPlayerMessage message : messages) { - playerMessages.add( - createMessage(message.target) - .setType(message.messageType) - .setPayload(message.message) - .send()); - } - boolean wasInterrupted = false; - for (PlayerMessage message : playerMessages) { - boolean blockMessage = true; - while (blockMessage) { - try { - message.blockUntilDelivered(); - blockMessage = false; - } catch (InterruptedException e) { - wasInterrupted = true; - } - } - } - if (wasInterrupted) { - // Restore the interrupted status. - Thread.currentThread().interrupt(); - } - } - @Override public int getCurrentPeriodIndex() { if (shouldMaskPosition()) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 1c6458c551..b427991d6e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -1034,26 +1034,12 @@ public class SimpleExoPlayer extends BasePlayer currentCues = Collections.emptyList(); } - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void sendMessages(ExoPlayerMessage... messages) { - player.sendMessages(messages); - } - @Override public PlayerMessage createMessage(PlayerMessage.Target target) { verifyApplicationThread(); return player.createMessage(target); } - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void blockingSendMessages(ExoPlayerMessage... messages) { - player.blockingSendMessages(messages); - } - @Override public int getRendererCount() { verifyApplicationThread(); diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 56de0a8b33..df96b634dd 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -175,20 +175,6 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void sendMessages(ExoPlayerMessage... messages) { - throw new UnsupportedOperationException(); - } - - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void blockingSendMessages(ExoPlayerMessage... messages) { - throw new UnsupportedOperationException(); - } - @Override public int getRendererCount() { throw new UnsupportedOperationException(); From 1d36edc2148234cd8023fefd9b7620e7a7d5c76b Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 27 Jun 2019 13:26:47 +0100 Subject: [PATCH 1397/1556] Remove unnecessary FileDescriptor sync PiperOrigin-RevId: 255380796 --- .../exoplayer2/upstream/cache/CacheDataSink.java | 16 ---------------- .../upstream/cache/CacheDataSinkFactory.java | 16 +--------------- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 3de52b560c..80fecf19cc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -49,7 +49,6 @@ public final class CacheDataSink implements DataSink { private final long fragmentSize; private final int bufferSize; - private boolean syncFileDescriptor; private DataSpec dataSpec; private long dataSpecFragmentSize; private File file; @@ -108,18 +107,6 @@ public final class CacheDataSink implements DataSink { this.cache = Assertions.checkNotNull(cache); this.fragmentSize = fragmentSize == C.LENGTH_UNSET ? Long.MAX_VALUE : fragmentSize; this.bufferSize = bufferSize; - syncFileDescriptor = true; - } - - /** - * Sets whether file descriptors are synced when closing output streams. - * - *

      This method is experimental, and will be renamed or removed in a future release. - * - * @param syncFileDescriptor Whether file descriptors are synced when closing output streams. - */ - public void experimental_setSyncFileDescriptor(boolean syncFileDescriptor) { - this.syncFileDescriptor = syncFileDescriptor; } @Override @@ -207,9 +194,6 @@ public final class CacheDataSink implements DataSink { boolean success = false; try { outputStream.flush(); - if (syncFileDescriptor) { - underlyingFileOutputStream.getFD().sync(); - } success = true; } finally { Util.closeQuietly(outputStream); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java index 856e9db168..ce9735badd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java @@ -26,8 +26,6 @@ public final class CacheDataSinkFactory implements DataSink.Factory { private final long fragmentSize; private final int bufferSize; - private boolean syncFileDescriptor; - /** @see CacheDataSink#CacheDataSink(Cache, long) */ public CacheDataSinkFactory(Cache cache, long fragmentSize) { this(cache, fragmentSize, CacheDataSink.DEFAULT_BUFFER_SIZE); @@ -40,20 +38,8 @@ public final class CacheDataSinkFactory implements DataSink.Factory { this.bufferSize = bufferSize; } - /** - * See {@link CacheDataSink#experimental_setSyncFileDescriptor(boolean)}. - * - *

      This method is experimental, and will be renamed or removed in a future release. - */ - public CacheDataSinkFactory experimental_setSyncFileDescriptor(boolean syncFileDescriptor) { - this.syncFileDescriptor = syncFileDescriptor; - return this; - } - @Override public DataSink createDataSink() { - CacheDataSink dataSink = new CacheDataSink(cache, fragmentSize, bufferSize); - dataSink.experimental_setSyncFileDescriptor(syncFileDescriptor); - return dataSink; + return new CacheDataSink(cache, fragmentSize, bufferSize); } } From cf68d4eb474d8ece404e6e0d920826832eeb0a28 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 27 Jun 2019 13:28:18 +0100 Subject: [PATCH 1398/1556] Cleanup: Remove deprecated text and metadata output interfaces PiperOrigin-RevId: 255380951 --- .../android/exoplayer2/metadata/MetadataRenderer.java | 6 ------ .../com/google/android/exoplayer2/text/TextRenderer.java | 6 ------ 2 files changed, 12 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index a72c70442e..a775481633 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -34,12 +34,6 @@ import java.util.Arrays; */ public final class MetadataRenderer extends BaseRenderer implements Callback { - /** - * @deprecated Use {@link MetadataOutput}. - */ - @Deprecated - public interface Output extends MetadataOutput {} - private static final int MSG_INVOKE_RENDERER = 0; // TODO: Holding multiple pending metadata objects is temporary mitigation against // https://github.com/google/ExoPlayer/issues/1874. It should be removed once this issue has been diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index bdf127be59..1622d68d99 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -44,12 +44,6 @@ import java.util.List; */ public final class TextRenderer extends BaseRenderer implements Callback { - /** - * @deprecated Use {@link TextOutput}. - */ - @Deprecated - public interface Output extends TextOutput {} - @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({ From 1bd73eb70ed4341acb9dfd815665ddf683100e57 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 27 Jun 2019 16:52:57 +0100 Subject: [PATCH 1399/1556] Cleanup: Remove DynamicConcatenatingMediaSource PiperOrigin-RevId: 255410268 --- .../source/ConcatenatingMediaSource.java | 57 +++++++++---------- .../DynamicConcatenatingMediaSource.java | 46 --------------- 2 files changed, 28 insertions(+), 75 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java 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 c031fcde21..c2ec437d84 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 @@ -46,7 +46,7 @@ import java.util.Set; * during playback. It is valid for the same {@link MediaSource} instance to be present more than * once in the concatenation. Access to this class is thread-safe. */ -public class ConcatenatingMediaSource extends CompositeMediaSource { +public final class ConcatenatingMediaSource extends CompositeMediaSource { private static final int MSG_ADD = 0; private static final int MSG_REMOVE = 1; @@ -149,7 +149,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources) { + public synchronized void addMediaSources(Collection mediaSources) { addPublicMediaSources( mediaSourcesPublic.size(), mediaSources, @@ -221,7 +221,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources, Handler handler, Runnable onCompletionAction) { addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, onCompletionAction); } @@ -234,7 +234,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources) { + public synchronized void addMediaSources(int index, Collection mediaSources) { addPublicMediaSources(index, mediaSources, /* handler= */ null, /* onCompletionAction= */ null); } @@ -249,7 +249,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources, Handler handler, @@ -269,7 +269,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource Date: Thu, 27 Jun 2019 17:03:54 +0100 Subject: [PATCH 1400/1556] Visibility clean-up: Don't extend visibility of protected methods in overrides. PiperOrigin-RevId: 255412493 --- .../google/android/exoplayer2/source/ClippingMediaSource.java | 4 ++-- .../android/exoplayer2/source/CompositeMediaSource.java | 4 ++-- .../android/exoplayer2/source/ExtractorMediaSource.java | 4 ++-- .../google/android/exoplayer2/source/LoopingMediaSource.java | 2 +- .../google/android/exoplayer2/source/MergingMediaSource.java | 4 ++-- .../android/exoplayer2/source/ProgressiveMediaSource.java | 4 ++-- .../google/android/exoplayer2/source/SilenceMediaSource.java | 4 ++-- .../android/exoplayer2/source/SingleSampleMediaSource.java | 4 ++-- .../google/android/exoplayer2/source/ads/AdsMediaSource.java | 4 ++-- .../android/exoplayer2/source/dash/DashMediaSource.java | 4 ++-- .../google/android/exoplayer2/source/hls/HlsMediaSource.java | 4 ++-- .../exoplayer2/source/smoothstreaming/SsMediaSource.java | 4 ++-- .../google/android/exoplayer2/testutil/FakeMediaSource.java | 2 +- 13 files changed, 24 insertions(+), 24 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index c3e700fff5..c942f9320e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -193,7 +193,7 @@ public final class ClippingMediaSource extends CompositeMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); prepareChildSource(/* id= */ null, mediaSource); } @@ -228,7 +228,7 @@ public final class ClippingMediaSource extends CompositeMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { super.releaseSourceInternal(); clippingError = null; clippingTimeline = null; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java index 06db088f06..1a9e1ff250 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java @@ -44,7 +44,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { @Override @CallSuper - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { this.mediaTransferListener = mediaTransferListener; eventHandler = new Handler(); } @@ -59,7 +59,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { @Override @CallSuper - public void releaseSourceInternal() { + protected void releaseSourceInternal() { for (MediaSourceAndListener childSource : childSources.values()) { childSource.mediaSource.releaseSource(childSource.listener); childSource.mediaSource.removeEventListener(childSource.eventListener); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index d9003e443e..f07ee63e79 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -339,7 +339,7 @@ public final class ExtractorMediaSource extends BaseMediaSource } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { progressiveMediaSource.prepareSource(/* listener= */ this, mediaTransferListener); } @@ -359,7 +359,7 @@ public final class ExtractorMediaSource extends BaseMediaSource } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { progressiveMediaSource.releaseSource(/* listener= */ this); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 769f545aaa..7adb18dc94 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -71,7 +71,7 @@ public final class LoopingMediaSource extends CompositeMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); prepareChildSource(/* id= */ null, childSource); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index 7188cada0f..f12ce92f54 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -104,7 +104,7 @@ public final class MergingMediaSource extends CompositeMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); for (int i = 0; i < mediaSources.length; i++) { prepareChildSource(i, mediaSources[i]); @@ -140,7 +140,7 @@ public final class MergingMediaSource extends CompositeMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { super.releaseSourceInternal(); Arrays.fill(timelines, null); primaryManifest = null; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java index 5ed12154b3..ba69b46d7f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -228,7 +228,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { transferListener = mediaTransferListener; notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable); } @@ -262,7 +262,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { // Do nothing. } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java index b03dd0ea7c..fc99e8cb7b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java @@ -66,7 +66,7 @@ public final class SilenceMediaSource extends BaseMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { refreshSourceInfo( new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false), /* manifest= */ null); @@ -84,7 +84,7 @@ public final class SilenceMediaSource extends BaseMediaSource { public void releasePeriod(MediaPeriod mediaPeriod) {} @Override - public void releaseSourceInternal() {} + protected void releaseSourceInternal() {} private static final class SilenceMediaPeriod implements MediaPeriod { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 55d967cd69..6c1881a01a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -302,7 +302,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { transferListener = mediaTransferListener; refreshSourceInfo(timeline, /* manifest= */ null); } @@ -331,7 +331,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { // Do nothing. } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index dd4c0d26b2..a6c2cf2767 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -193,7 +193,7 @@ public final class AdsMediaSource extends CompositeMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); ComponentListener componentListener = new ComponentListener(); this.componentListener = componentListener; @@ -259,7 +259,7 @@ public final class AdsMediaSource extends CompositeMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { super.releaseSourceInternal(); Assertions.checkNotNull(componentListener).release(); componentListener = null; 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 779a97fd09..551555502f 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 @@ -630,7 +630,7 @@ public final class DashMediaSource extends BaseMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { this.mediaTransferListener = mediaTransferListener; if (sideloadedManifest) { processManifest(false); @@ -679,7 +679,7 @@ public final class DashMediaSource extends BaseMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { manifestLoadPending = false; dataSource = null; if (loader != null) { 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 fbb6285d1d..f891670e78 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 @@ -333,7 +333,7 @@ public final class HlsMediaSource extends BaseMediaSource } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { this.mediaTransferListener = mediaTransferListener; EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); playlistTracker.start(manifestUri, eventDispatcher, /* listener= */ this); @@ -366,7 +366,7 @@ public final class HlsMediaSource extends BaseMediaSource } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { playlistTracker.stop(); } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 3c18bfe644..e31fbccae5 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -525,7 +525,7 @@ public final class SsMediaSource extends BaseMediaSource } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { this.mediaTransferListener = mediaTransferListener; if (sideloadedManifest) { manifestLoaderErrorThrower = new LoaderErrorThrower.Dummy(); @@ -568,7 +568,7 @@ public final class SsMediaSource extends BaseMediaSource } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { manifest = sideloadedManifest ? manifest : null; manifestDataSource = null; manifestLoadStartTimestamp = 0; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index 0d50f22bc0..80456169ff 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -137,7 +137,7 @@ public class FakeMediaSource extends BaseMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { assertThat(preparedSource).isTrue(); assertThat(releasedSource).isFalse(); assertThat(activeMediaPeriods.isEmpty()).isTrue(); From 244c202c5667600e1e6613da426f01018ceb20a4 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 27 Jun 2019 19:26:56 +0100 Subject: [PATCH 1401/1556] Fix hidden API warnings from Metalava PiperOrigin-RevId: 255442455 --- .../exoplayer2/drm/DefaultDrmSession.java | 2 +- .../exoplayer2/extractor/ts/H262Reader.java | 2 +- .../exoplayer2/extractor/ts/SeiReader.java | 6 ++-- .../exoplayer2/text/dvb/DvbDecoder.java | 3 +- .../exoplayer2/text/ssa/SsaDecoder.java | 3 +- .../exoplayer2/text/subrip/SubripDecoder.java | 9 +++--- .../exoplayer2/text/ttml/TtmlDecoder.java | 3 +- .../text/webvtt/Mp4WebvttDecoder.java | 3 +- .../exoplayer2/text/webvtt/WebvttDecoder.java | 3 +- .../upstream/cache/SimpleCache.java | 6 ++-- .../exoplayer2/text/ssa/SsaDecoderTest.java | 17 ++++++----- .../text/subrip/SubripDecoderTest.java | 29 ++++++++++--------- .../exoplayer2/text/ttml/TtmlDecoderTest.java | 2 +- .../text/webvtt/WebvttDecoderTest.java | 2 +- .../upstream/cache/CacheDataSourceTest.java | 2 +- .../upstream/cache/SimpleCacheTest.java | 2 +- 16 files changed, 50 insertions(+), 44 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 e49602957f..c83214c8d5 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 @@ -45,7 +45,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** A {@link DrmSession} that supports playbacks using {@link ExoMediaDrm}. */ @TargetApi(18) -/* package */ class DefaultDrmSession implements DrmSession { +public class DefaultDrmSession implements DrmSession { /** Manages provisioning requests. */ public interface ProvisioningManager { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index 1564157d44..e7f2c1935b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -72,7 +72,7 @@ public final class H262Reader implements ElementaryStreamReader { this(null); } - public H262Reader(UserDataReader userDataReader) { + /* package */ H262Reader(UserDataReader userDataReader) { this.userDataReader = userDataReader; prefixFlags = new boolean[4]; csdBuffer = new CsdBuffer(128); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java index 895c224697..d032ef5883 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -26,10 +26,8 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.List; -/** - * Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}. - */ -/* package */ final class SeiReader { +/** Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}. */ +public final class SeiReader { private final List closedCaptionFormats; private final TrackOutput[] outputs; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java index df5b19c052..22ce893fce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.text.dvb; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.List; @@ -38,7 +39,7 @@ public final class DvbDecoder extends SimpleSubtitleDecoder { } @Override - protected DvbSubtitle decode(byte[] data, int length, boolean reset) { + protected Subtitle decode(byte[] data, int length, boolean reset) { if (reset) { parser.reset(); } 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 c25b26128c..d701f99d73 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 @@ -19,6 +19,7 @@ import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.LongArray; @@ -72,7 +73,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { } @Override - protected SsaSubtitle decode(byte[] bytes, int length, boolean reset) { + protected Subtitle decode(byte[] bytes, int length, boolean reset) { ArrayList cues = new ArrayList<>(); LongArray cueTimesUs = new LongArray(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index 6f9fd366ec..cf174283ec 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -21,6 +21,7 @@ import android.text.Spanned; import android.text.TextUtils; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.LongArray; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -34,9 +35,9 @@ import java.util.regex.Pattern; public final class SubripDecoder extends SimpleSubtitleDecoder { // Fractional positions for use when alignment tags are present. - /* package */ static final float START_FRACTION = 0.08f; - /* package */ static final float END_FRACTION = 1 - START_FRACTION; - /* package */ static final float MID_FRACTION = 0.5f; + private static final float START_FRACTION = 0.08f; + private static final float END_FRACTION = 1 - START_FRACTION; + private static final float MID_FRACTION = 0.5f; private static final String TAG = "SubripDecoder"; @@ -68,7 +69,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } @Override - protected SubripSubtitle decode(byte[] bytes, int length, boolean reset) { + protected Subtitle decode(byte[] bytes, int length, boolean reset) { ArrayList cues = new ArrayList<>(); LongArray cueTimesUs = new LongArray(); ParsableByteArray subripData = new ParsableByteArray(bytes, length); 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 6e0c495466..6dabcdd904 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 @@ -19,6 +19,7 @@ import android.text.Layout; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.Log; @@ -102,7 +103,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } @Override - protected TtmlSubtitle decode(byte[] bytes, int length, boolean reset) + protected Subtitle decode(byte[] bytes, int length, boolean reset) throws SubtitleDecoderException { try { XmlPullParser xmlParser = xmlParserFactory.newPullParser(); 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 b977f61a8a..8b255ac2bd 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.text.webvtt; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -49,7 +50,7 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { } @Override - protected Mp4WebvttSubtitle decode(byte[] bytes, int length, boolean reset) + protected Subtitle decode(byte[] bytes, int length, boolean reset) throws SubtitleDecoderException { // Webvtt in Mp4 samples have boxes inside of them, so we have to do a traditional box parsing: // first 4 bytes size and then 4 bytes type. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java index fe3c86bd1e..9b356f0988 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.text.webvtt; import android.text.TextUtils; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; @@ -55,7 +56,7 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { } @Override - protected WebvttSubtitle decode(byte[] bytes, int length, boolean reset) + protected Subtitle decode(byte[] bytes, int length, boolean reset) throws SubtitleDecoderException { parsableWebvttData.reset(bytes, length); // Initialization for consistent starting state. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index ea37612c88..81212b731f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -380,13 +380,13 @@ public final class SimpleCache implements Cache { } @Override - public synchronized SimpleCacheSpan startReadWrite(String key, long position) + public synchronized CacheSpan startReadWrite(String key, long position) throws InterruptedException, CacheException { Assertions.checkState(!released); checkInitialization(); while (true) { - SimpleCacheSpan span = startReadWriteNonBlocking(key, position); + CacheSpan span = startReadWriteNonBlocking(key, position); if (span != null) { return span; } else { @@ -402,7 +402,7 @@ public final class SimpleCache implements Cache { @Override @Nullable - public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position) + public synchronized CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException { Assertions.checkState(!released); checkInitialization(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java index ab67ac115b..7095962801 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.text.Subtitle; import java.io.IOException; import java.util.ArrayList; import org.junit.Test; @@ -41,7 +42,7 @@ public final class SsaDecoderTest { public void testDecodeEmpty() throws IOException { SsaDecoder decoder = new SsaDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY); - SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(0); assertThat(subtitle.getCues(0).isEmpty()).isTrue(); @@ -51,7 +52,7 @@ public final class SsaDecoderTest { public void testDecodeTypical() throws IOException { SsaDecoder decoder = new SsaDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL); - SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); assertTypicalCue1(subtitle, 0); @@ -71,7 +72,7 @@ public final class SsaDecoderTest { SsaDecoder decoder = new SsaDecoder(initializationData); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_DIALOGUE_ONLY); - SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); assertTypicalCue1(subtitle, 0); @@ -85,7 +86,7 @@ public final class SsaDecoderTest { SsaDecoder decoder = new SsaDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INVALID_TIMECODES); - SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(2); assertTypicalCue3(subtitle, 0); @@ -96,7 +97,7 @@ public final class SsaDecoderTest { SsaDecoder decoder = new SsaDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NO_END_TIMECODES); - SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(3); @@ -113,21 +114,21 @@ public final class SsaDecoderTest { .isEqualTo("This is the third subtitle, with a comma."); } - private static void assertTypicalCue1(SsaSubtitle subtitle, int eventIndex) { + private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) .isEqualTo("This is the first subtitle."); assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(1230000); } - private static void assertTypicalCue2(SsaSubtitle subtitle, int eventIndex) { + private static void assertTypicalCue2(Subtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(2340000); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) .isEqualTo("This is the second subtitle \nwith a newline \nand another."); assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(3450000); } - private static void assertTypicalCue3(SsaSubtitle subtitle, int eventIndex) { + private static void assertTypicalCue3(Subtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(4560000); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) .isEqualTo("This is the third subtitle, with a comma."); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java index 9520262207..774e8d98b9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java @@ -21,6 +21,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.Subtitle; import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; @@ -44,7 +45,7 @@ public final class SubripDecoderTest { public void testDecodeEmpty() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(0); assertThat(subtitle.getCues(0).isEmpty()).isTrue(); @@ -54,7 +55,7 @@ public final class SubripDecoderTest { public void testDecodeTypical() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_FILE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); assertTypicalCue1(subtitle, 0); @@ -68,7 +69,7 @@ public final class SubripDecoderTest { byte[] bytes = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), TYPICAL_WITH_BYTE_ORDER_MARK); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); assertTypicalCue1(subtitle, 0); @@ -82,7 +83,7 @@ public final class SubripDecoderTest { byte[] bytes = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), TYPICAL_EXTRA_BLANK_LINE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); assertTypicalCue1(subtitle, 0); @@ -97,7 +98,7 @@ public final class SubripDecoderTest { byte[] bytes = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_TIMECODE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); assertTypicalCue1(subtitle, 0); @@ -111,7 +112,7 @@ public final class SubripDecoderTest { byte[] bytes = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_SEQUENCE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); assertTypicalCue1(subtitle, 0); @@ -125,7 +126,7 @@ public final class SubripDecoderTest { byte[] bytes = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), TYPICAL_NEGATIVE_TIMESTAMPS); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(2); assertTypicalCue3(subtitle, 0); @@ -137,7 +138,7 @@ public final class SubripDecoderTest { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UNEXPECTED_END); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); assertTypicalCue1(subtitle, 0); @@ -149,7 +150,7 @@ public final class SubripDecoderTest { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NO_END_TIMECODES_FILE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(3); @@ -171,7 +172,7 @@ public final class SubripDecoderTest { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_WITH_TAGS); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertTypicalCue1(subtitle, 0); assertTypicalCue2(subtitle, 2); @@ -194,21 +195,21 @@ public final class SubripDecoderTest { assertAlignmentCue(subtitle, 26, Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_END); // {/an9} } - private static void assertTypicalCue1(SubripSubtitle subtitle, int eventIndex) { + private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) .isEqualTo("This is the first subtitle."); assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(1234000); } - private static void assertTypicalCue2(SubripSubtitle subtitle, int eventIndex) { + private static void assertTypicalCue2(Subtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(2345000); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) .isEqualTo("This is the second subtitle.\nSecond subtitle with second line."); assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(3456000); } - private static void assertTypicalCue3(SubripSubtitle subtitle, int eventIndex) { + private static void assertTypicalCue3(Subtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(4567000); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) .isEqualTo("This is the third subtitle."); @@ -216,7 +217,7 @@ public final class SubripDecoderTest { } private static void assertAlignmentCue( - SubripSubtitle subtitle, + Subtitle subtitle, int eventIndex, @Cue.AnchorType int lineAnchor, @Cue.AnchorType int positionAnchor) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java index 85af6482c0..22c7288340 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java @@ -701,6 +701,6 @@ public final class TtmlDecoderTest { private TtmlSubtitle getSubtitle(String file) throws IOException, SubtitleDecoderException { TtmlDecoder ttmlDecoder = new TtmlDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), file); - return ttmlDecoder.decode(bytes, bytes.length, false); + return (TtmlSubtitle) ttmlDecoder.decode(bytes, bytes.length, false); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index 2a7289c039..9320a3f31c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -395,7 +395,7 @@ public class WebvttDecoderTest { throws IOException, SubtitleDecoderException { WebvttDecoder decoder = new WebvttDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), asset); - return decoder.decode(bytes, bytes.length, /* reset= */ false); + return (WebvttSubtitle) decoder.decode(bytes, bytes.length, /* reset= */ false); } private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index 956a5fc283..83104119ad 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -363,7 +363,7 @@ public final class CacheDataSourceTest { .appendReadData(1); // Lock the content on the cache. - SimpleCacheSpan cacheSpan = cache.startReadWriteNonBlocking(defaultCacheKey, 0); + CacheSpan cacheSpan = cache.startReadWriteNonBlocking(defaultCacheKey, 0); assertThat(cacheSpan).isNotNull(); assertThat(cacheSpan.isHoleSpan()).isTrue(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index 3d684aab82..fc229d9dc6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -164,7 +164,7 @@ public class SimpleCacheTest { .isEqualTo(150); // Removing the last span shouldn't cause the length be change next time cache loaded - SimpleCacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145); + CacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145); simpleCache2.removeSpan(lastSpan); simpleCache2.release(); simpleCache2 = getSimpleCache(); From ae0aeb046b11cc21a2f8470f5eb2f6cb211a0e1e Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 27 Jun 2019 21:51:41 +0100 Subject: [PATCH 1402/1556] call setPlayWhenReady in any case ISSUE: #6093 PiperOrigin-RevId: 255471282 --- .../exoplayer2/ext/mediasession/MediaSessionConnector.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 9ec3886df5..eaebf8b4e1 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 @@ -1089,8 +1089,9 @@ public final class MediaSessionConnector { } } else if (player.getPlaybackState() == Player.STATE_ENDED) { controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); - controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true); } + controlDispatcher.dispatchSetPlayWhenReady( + Assertions.checkNotNull(player), /* playWhenReady= */ true); } } From 6fe70ca43d982ad51ee30b7afbb26ca079b19c84 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 28 Jun 2019 13:21:43 +0100 Subject: [PATCH 1403/1556] Use the floor of the frame rate for capability checks PiperOrigin-RevId: 255584000 --- .../exoplayer2/mediacodec/MediaCodecInfo.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index e79c776f88..3310b0dc8b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -520,9 +520,15 @@ public final class MediaCodecInfo { @TargetApi(21) private static boolean areSizeAndRateSupportedV21(VideoCapabilities capabilities, int width, int height, double frameRate) { - return frameRate == Format.NO_VALUE || frameRate <= 0 - ? capabilities.isSizeSupported(width, height) - : capabilities.areSizeAndRateSupported(width, height, frameRate); + if (frameRate == Format.NO_VALUE || frameRate <= 0) { + return capabilities.isSizeSupported(width, height); + } else { + // The signaled frame rate may be slightly higher than the actual frame rate, so we take the + // floor to avoid situations where a range check in areSizeAndRateSupported fails due to + // slightly exceeding the limits for a standard format (e.g., 1080p at 30 fps). + double floorFrameRate = Math.floor(frameRate); + return capabilities.areSizeAndRateSupported(width, height, floorFrameRate); + } } @TargetApi(23) From 71de1d37ac58ce28df70c8fa3017b0688c0cf266 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 1 Jul 2019 13:03:07 +0100 Subject: [PATCH 1404/1556] Don't consume touch events if no controller is attached. Issue:#6109 PiperOrigin-RevId: 255933121 --- .../com/google/android/exoplayer2/ui/PlayerView.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 7e01801daf..269c48c282 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 @@ -1050,6 +1050,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Override public boolean onTouchEvent(MotionEvent event) { + if (!useController || player == null) { + return false; + } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: isTouching = true; @@ -1150,9 +1153,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider // Internal methods. private boolean toggleControllerVisibility() { - if (!useController || player == null) { - return false; - } if (!controller.isVisible()) { maybeShowController(true); } else if (controllerHideOnTouch) { @@ -1471,6 +1471,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Override public boolean onSingleTapUp(MotionEvent e) { + if (!useController || player == null) { + return false; + } return toggleControllerVisibility(); } } From 04959ec648389d6ce71cabf54dc0e8bc1fcfe22d Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 1 Jul 2019 14:49:35 +0100 Subject: [PATCH 1405/1556] Remove unnecessary variables from ConcatenatingMediaSource. The total window and period count, as well as the period offset for each holder are not actually needed and can be removed. Also added a TODO to remove two other variables if possible. PiperOrigin-RevId: 255945584 --- .../source/ConcatenatingMediaSource.java | 85 +++++++------------ 1 file changed, 31 insertions(+), 54 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 c2ec437d84..bdf55fe40d 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 @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source; import android.os.Handler; import android.os.Message; import androidx.annotation.GuardedBy; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import android.util.Pair; import com.google.android.exoplayer2.C; @@ -78,8 +77,6 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource nextTimelineUpdateOnCompletionActions; private ShuffleOrder shuffleOrder; - private int windowCount; - private int periodCount; /** * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same @@ -483,8 +480,6 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource onCompletionActions = nextTimelineUpdateOnCompletionActions; nextTimelineUpdateOnCompletionActions = new HashSet<>(); refreshSourceInfo( - new ConcatenatedTimeline( - mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), - /* manifest= */ null); + new ConcatenatedTimeline(mediaSourceHolders, shuffleOrder, isAtomic), /* manifest= */ null); getPlaybackThreadHandlerOnPlaybackThread() .obtainMessage(MSG_ON_COMPLETION, onCompletionActions) .sendToTarget(); @@ -737,17 +730,12 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource { + /* package */ static final class MediaSourceHolder { public final MediaSource mediaSource; public final Object uid; @@ -901,7 +883,6 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceHolders, - int windowCount, - int periodCount, ShuffleOrder shuffleOrder, boolean isAtomic) { super(isAtomic, shuffleOrder); - this.windowCount = windowCount; - this.periodCount = periodCount; int childCount = mediaSourceHolders.size(); firstPeriodInChildIndices = new int[childCount]; firstWindowInChildIndices = new int[childCount]; @@ -970,13 +941,19 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource(); int index = 0; + int windowCount = 0; + int periodCount = 0; for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { timelines[index] = mediaSourceHolder.timeline; - firstPeriodInChildIndices[index] = mediaSourceHolder.firstPeriodIndexInChild; - firstWindowInChildIndices[index] = mediaSourceHolder.firstWindowIndexInChild; + firstWindowInChildIndices[index] = windowCount; + firstPeriodInChildIndices[index] = periodCount; + windowCount += timelines[index].getWindowCount(); + periodCount += timelines[index].getPeriodCount(); uids[index] = mediaSourceHolder.uid; childIndexByUid.put(uids[index], index++); } + this.windowCount = windowCount; + this.periodCount = periodCount; } @Override From 7798c07f64d58a5363ff2af1a72cdcad9a8ae2de Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 1 Jul 2019 16:52:36 +0100 Subject: [PATCH 1406/1556] Remove ExoCast PiperOrigin-RevId: 255964199 --- cast_receiver_app/BUILD | 310 ---- cast_receiver_app/README.md | 72 - cast_receiver_app/WORKSPACE | 38 - cast_receiver_app/app-desktop/html/index.css | 156 -- cast_receiver_app/app-desktop/html/index.html | 55 - cast_receiver_app/app-desktop/src/main.js | 170 -- .../app-desktop/src/player_controls.js | 164 -- cast_receiver_app/app-desktop/src/samples.js | 70 - .../app-desktop/src/samples_internal.js | 79 - cast_receiver_app/app/html/index.css | 39 - cast_receiver_app/app/html/index.html | 40 - .../app/html/playback_info_view.css | 59 - cast_receiver_app/app/src/main.js | 55 - .../app/src/message_dispatcher.js | 234 --- cast_receiver_app/app/src/receiver.js | 191 --- cast_receiver_app/app/src/validation.js | 163 -- cast_receiver_app/assemble.bazel.sh | 93 - cast_receiver_app/externs/protocol.js | 489 ------ cast_receiver_app/externs/shaka.js | 68 - .../src/configuration_factory.js | 90 - cast_receiver_app/src/constants.js | 140 -- cast_receiver_app/src/playback_info_view.js | 233 --- cast_receiver_app/src/player.js | 1522 ----------------- cast_receiver_app/src/timeout.js | 68 - cast_receiver_app/src/util.js | 62 - cast_receiver_app/test/caf_bootstrap.js | 33 - .../test/configuration_factory_test.js | 86 - cast_receiver_app/test/externs.js | 36 - .../test/message_dispatcher_test.js | 49 - cast_receiver_app/test/mocks.js | 277 --- .../test/playback_info_view_test.js | 242 --- cast_receiver_app/test/player_test.js | 470 ----- cast_receiver_app/test/queue_test.js | 166 -- cast_receiver_app/test/receiver_test.js | 1027 ----------- .../test/shaka_error_handling_test.js | 84 - cast_receiver_app/test/util.js | 87 - cast_receiver_app/test/validation_test.js | 266 --- .../DefaultReceiverPlayerManager.java | 437 ----- .../android/exoplayer2/castdemo/DemoUtil.java | 5 + .../castdemo/ExoCastPlayerManager.java | 421 ----- .../exoplayer2/castdemo/MainActivity.java | 35 +- .../exoplayer2/castdemo/PlayerManager.java | 422 ++++- demos/cast/src/main/res/values/strings.xml | 2 - .../ext/cast/CastSessionManager.java | 86 - .../ext/cast/DefaultCastOptionsProvider.java | 2 +- .../ext/cast/DefaultCastSessionManager.java | 187 -- .../exoplayer2/ext/cast/ExoCastConstants.java | 118 -- .../exoplayer2/ext/cast/ExoCastMessage.java | 474 ----- .../ext/cast/ExoCastOptionsProvider.java | 40 - .../exoplayer2/ext/cast/ExoCastPlayer.java | 958 ----------- .../exoplayer2/ext/cast/ExoCastTimeline.java | 342 ---- .../exoplayer2/ext/cast/MediaItemInfo.java | 160 -- .../exoplayer2/ext/cast/MediaItemQueue.java | 85 - .../ext/cast/ReceiverAppStateUpdate.java | 633 ------- .../ext/cast/ExoCastMessageTest.java | 436 ----- .../ext/cast/ExoCastPlayerTest.java | 1018 ----------- .../ext/cast/ExoCastTimelineTest.java | 466 ----- .../ext/cast/ReceiverAppStateUpdateTest.java | 378 ---- 58 files changed, 407 insertions(+), 13781 deletions(-) delete mode 100644 cast_receiver_app/BUILD delete mode 100644 cast_receiver_app/README.md delete mode 100644 cast_receiver_app/WORKSPACE delete mode 100644 cast_receiver_app/app-desktop/html/index.css delete mode 100644 cast_receiver_app/app-desktop/html/index.html delete mode 100644 cast_receiver_app/app-desktop/src/main.js delete mode 100644 cast_receiver_app/app-desktop/src/player_controls.js delete mode 100644 cast_receiver_app/app-desktop/src/samples.js delete mode 100644 cast_receiver_app/app-desktop/src/samples_internal.js delete mode 100644 cast_receiver_app/app/html/index.css delete mode 100644 cast_receiver_app/app/html/index.html delete mode 100644 cast_receiver_app/app/html/playback_info_view.css delete mode 100644 cast_receiver_app/app/src/main.js delete mode 100644 cast_receiver_app/app/src/message_dispatcher.js delete mode 100644 cast_receiver_app/app/src/receiver.js delete mode 100644 cast_receiver_app/app/src/validation.js delete mode 100755 cast_receiver_app/assemble.bazel.sh delete mode 100644 cast_receiver_app/externs/protocol.js delete mode 100644 cast_receiver_app/externs/shaka.js delete mode 100644 cast_receiver_app/src/configuration_factory.js delete mode 100644 cast_receiver_app/src/constants.js delete mode 100644 cast_receiver_app/src/playback_info_view.js delete mode 100644 cast_receiver_app/src/player.js delete mode 100644 cast_receiver_app/src/timeout.js delete mode 100644 cast_receiver_app/src/util.js delete mode 100644 cast_receiver_app/test/caf_bootstrap.js delete mode 100644 cast_receiver_app/test/configuration_factory_test.js delete mode 100644 cast_receiver_app/test/externs.js delete mode 100644 cast_receiver_app/test/message_dispatcher_test.js delete mode 100644 cast_receiver_app/test/mocks.js delete mode 100644 cast_receiver_app/test/playback_info_view_test.js delete mode 100644 cast_receiver_app/test/player_test.js delete mode 100644 cast_receiver_app/test/queue_test.js delete mode 100644 cast_receiver_app/test/receiver_test.js delete mode 100644 cast_receiver_app/test/shaka_error_handling_test.js delete mode 100644 cast_receiver_app/test/util.js delete mode 100644 cast_receiver_app/test/validation_test.js delete mode 100644 demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java delete mode 100644 demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/ExoCastPlayerManager.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastSessionManager.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastSessionManager.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastConstants.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastMessage.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastOptionsProvider.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayer.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastTimeline.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemInfo.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemQueue.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java delete mode 100644 extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastMessageTest.java delete mode 100644 extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayerTest.java delete mode 100644 extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastTimelineTest.java delete mode 100644 extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdateTest.java diff --git a/cast_receiver_app/BUILD b/cast_receiver_app/BUILD deleted file mode 100644 index 2bd0526cdd..0000000000 --- a/cast_receiver_app/BUILD +++ /dev/null @@ -1,310 +0,0 @@ -# Copyright (C) 2019 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. - -load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library") -load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_binary") -load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_test") -load("@io_bazel_rules_closure//closure:defs.bzl", "closure_css_library") -load("@io_bazel_rules_closure//closure:defs.bzl", "closure_css_binary") - -licenses(["notice"]) # Apache 2.0 - -# The Shaka player library - 2.5.0-beta2 (needs to be cloned from Github). -closure_js_library( - name = "shaka_player_library", - srcs = glob( - [ - "external-js/shaka-player/lib/**/*.js", - "external-js/shaka-player/externs/**/*.js", - ], - exclude = [ - "external-js/shaka-player/lib/debug/asserts.js", - "external-js/shaka-player/externs/mediakeys.js", - "external-js/shaka-player/externs/networkinformation.js", - "external-js/shaka-player/externs/vtt_region.js", - ], - ), - suppress = [ - "strictMissingRequire", - "missingSourcesWarnings", - "analyzerChecks", - "strictCheckTypes", - "checkTypes", - ], - deps = [ - "@io_bazel_rules_closure//closure/library", - ], -) - -# The plain player not depending on the cast library. -closure_js_library( - name = "player_lib", - srcs = [ - "externs/protocol.js", - "src/configuration_factory.js", - "src/constants.js", - "src/playback_info_view.js", - "src/player.js", - "src/timeout.js", - "src/util.js", - ], - suppress = [ - "missingSourcesWarnings", - "analyzerChecks", - "strictCheckTypes", - ], - deps = [ - ":shaka_player_library", - "@io_bazel_rules_closure//closure/library", - ], -) - -# A debug app to test the player with a desktop browser. -closure_js_library( - name = "app_desktop_lib", - srcs = [ - "app-desktop/src/main.js", - "app-desktop/src/player_controls.js", - "app-desktop/src/samples.js", - "externs/shaka.js", - ], - suppress = [ - "reportUnknownTypes", - "strictCheckTypes", - ], - deps = [ - ":player_lib", - ":shaka_player_library", - "@io_bazel_rules_closure//closure/library", - ], -) - -# Includes the javascript files of the cast receiver app. -closure_js_library( - name = "app_lib", - srcs = [ - "app/src/main.js", - "app/src/message_dispatcher.js", - "app/src/receiver.js", - "app/src/validation.js", - "externs/cast.js", - "externs/shaka.js", - ], - suppress = [ - "missingSourcesWarnings", - "analyzerChecks", - "strictCheckTypes", - ], - deps = [ - ":player_lib", - ":shaka_player_library", - "@io_bazel_rules_closure//closure/library", - ], -) - -# Test utils like mocks. -closure_js_library( - name = "test_util_lib", - testonly = 1, - srcs = [ - "externs/protocol.js", - "test/externs.js", - "test/mocks.js", - "test/util.js", - ], - suppress = [ - "checkTypes", - "strictCheckTypes", - "reportUnknownTypes", - "accessControls", - "analyzerChecks", - "missingSourcesWarnings", - ], - deps = [ - ":shaka_player_library", - "@io_bazel_rules_closure//closure/library", - "@io_bazel_rules_closure//closure/library/testing:jsunit", - ], -) - -# Unit test for the player. -closure_js_test( - name = "player_tests", - srcs = glob([ - "test/player_test.js", - ]), - entry_points = [ - "exoplayer.cast.test", - ], - suppress = [ - "checkTypes", - "strictCheckTypes", - "reportUnknownTypes", - "accessControls", - "analyzerChecks", - "missingSourcesWarnings", - ], - deps = [ - ":app_lib", - ":player_lib", - ":test_util_lib", - "@io_bazel_rules_closure//closure/library/testing:asserts", - "@io_bazel_rules_closure//closure/library/testing:jsunit", - "@io_bazel_rules_closure//closure/library/testing:testsuite", - ], -) - -# Unit test for the queue in the player. -closure_js_test( - name = "queue_tests", - srcs = glob([ - "test/queue_test.js", - ]), - entry_points = [ - "exoplayer.cast.test.queue", - ], - suppress = [ - "checkTypes", - "strictCheckTypes", - "reportUnknownTypes", - "accessControls", - "analyzerChecks", - "missingSourcesWarnings", - ], - deps = [ - ":app_lib", - ":player_lib", - ":test_util_lib", - "@io_bazel_rules_closure//closure/library/testing:asserts", - "@io_bazel_rules_closure//closure/library/testing:jsunit", - "@io_bazel_rules_closure//closure/library/testing:testsuite", - ], -) - -# Unit test for the receiver. -closure_js_test( - name = "receiver_tests", - srcs = glob([ - "test/receiver_test.js", - ]), - entry_points = [ - "exoplayer.cast.test.receiver", - ], - suppress = [ - "checkTypes", - "strictCheckTypes", - "reportUnknownTypes", - "accessControls", - "analyzerChecks", - "missingSourcesWarnings", - ], - deps = [ - ":app_lib", - ":player_lib", - ":test_util_lib", - "@io_bazel_rules_closure//closure/library/testing:asserts", - "@io_bazel_rules_closure//closure/library/testing:jsunit", - "@io_bazel_rules_closure//closure/library/testing:testsuite", - ], -) - -# Unit test for the validations. -closure_js_test( - name = "validation_tests", - srcs = [ - "test/validation_test.js", - ], - entry_points = [ - "exoplayer.cast.test.validation", - ], - suppress = [ - "checkTypes", - "strictCheckTypes", - "reportUnknownTypes", - "accessControls", - "analyzerChecks", - "missingSourcesWarnings", - ], - deps = [ - ":app_lib", - ":player_lib", - ":test_util_lib", - "@io_bazel_rules_closure//closure/library/testing:asserts", - "@io_bazel_rules_closure//closure/library/testing:jsunit", - "@io_bazel_rules_closure//closure/library/testing:testsuite", - ], -) - -# The receiver app as a compiled binary. -closure_js_binary( - name = "app", - entry_points = [ - "exoplayer.cast.app", - "shaka.dash.DashParser", - "shaka.hls.HlsParser", - "shaka.abr.SimpleAbrManager", - "shaka.net.HttpFetchPlugin", - "shaka.net.HttpXHRPlugin", - "shaka.media.AdaptationSetCriteria", - ], - deps = [":app_lib"], -) - -# The debug app for the player as a compiled binary. -closure_js_binary( - name = "app_desktop", - entry_points = [ - "exoplayer.cast.debug", - "exoplayer.cast.samples", - "shaka.dash.DashParser", - "shaka.hls.HlsParser", - "shaka.abr.SimpleAbrManager", - "shaka.net.HttpFetchPlugin", - "shaka.net.HttpXHRPlugin", - "shaka.media.AdaptationSetCriteria", - ], - deps = [":app_desktop_lib"], -) - -# Defines the css style of the receiver app. -closure_css_library( - name = "app_styles_lib", - srcs = [ - "app/html/index.css", - "app/html/playback_info_view.css", - ], -) - -# Defines the css styles of the debug app. -closure_css_library( - name = "app_desktop_styles_lib", - srcs = [ - "app-desktop/html/index.css", - "app/html/playback_info_view.css", - ], -) - -# Compiles the css styles of the receiver app. -closure_css_binary( - name = "app_styles", - renaming = False, - deps = ["app_styles_lib"], -) - -# Compiles the css styles of the debug app. -closure_css_binary( - name = "app_desktop_styles", - renaming = False, - deps = ["app_desktop_styles_lib"], -) diff --git a/cast_receiver_app/README.md b/cast_receiver_app/README.md deleted file mode 100644 index 6504cb4f94..0000000000 --- a/cast_receiver_app/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# ExoPlayer cast receiver # - -An HTML/JavaScript app which runs within a Google cast device and can be loaded -and controller by an Android app which uses the ExoPlayer cast extension -(https://github.com/google/ExoPlayer/tree/release-v2/extensions/cast). - -# Build the app # - -You can build and deploy the app to your web server and register the url as your -cast receiver app (see: https://developers.google.com/cast/docs/registration). - -Building the app compiles JavaScript and CSS files. Dead JavaScript code of the -app itself and their dependencies (like ShakaPlayer) is removed and the -remaining code is minimized. - -## Prerequisites ## - -1. Install the most recent bazel release (https://bazel.build/) which is at - least 0.22.0. - -From within the root of the exo_receiver_app project do the following steps: - -2. Clone shaka from GitHub into the directory external-js/shaka-player: -``` -# git clone https://github.com/google/shaka-player.git \ - external-js/shaka-player -``` - -## 1. Customize html page and css (optional) ## - -(Optional) Edit index.html. **Make sure you do not change the id of the video -element**. -(Optional) Customize main.css. - -## 2. Build javascript and css files ## -``` -# bazel build ... -``` -## 3. Assemble the receiver app ## -``` -# WEB_DEPLOY_DIR=www -# mkdir ${WEB_DEPLOY_DIR} -# cp bazel-bin/exo_receiver_app.js ${WEB_DEPLOY_DIR} -# cp bazel-bin/exo_receiver_styles_bin.css ${WEB_DEPLOY_DIR} -# cp html/index.html ${WEB_DEPLOY_DIR} -``` - -Deploy the content of ${WEB_DEPLOY_DIR} to your web server. - -## 4. Assemble the debug app (optional) ## - -Debugging the player in a cast device is a little bit cumbersome compared to -debugging in a desktop browser. For this reason there is a debug app which -contains the player parts which are not depending on the cast library in a -traditional HTML app which can be run in a desktop browser. - -``` -# WEB_DEPLOY_DIR=www -# mkdir ${WEB_DEPLOY_DIR} -# cp bazel-bin/debug_app.js ${WEB_DEPLOY_DIR} -# cp bazel-bin/debug_styles_bin.css ${WEB_DEPLOY_DIR} -# cp html/player.html ${WEB_DEPLOY_DIR} -``` - -Deploy the content of ${WEB_DEPLOY_DIR} to your web server. - -# Unit test - -Unit tests can be run by the command -``` -# bazel test ... -``` diff --git a/cast_receiver_app/WORKSPACE b/cast_receiver_app/WORKSPACE deleted file mode 100644 index e6be3b9026..0000000000 --- a/cast_receiver_app/WORKSPACE +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (C) 2019 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. - -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") - -http_archive( - name = "com_google_protobuf", - sha256 = "73fdad358857e120fd0fa19e071a96e15c0f23bb25f85d3f7009abfd4f264a2a", - strip_prefix = "protobuf-3.6.1.3", - urls = ["https://github.com/google/protobuf/archive/v3.6.1.3.tar.gz"], -) - -http_archive( - name = "io_bazel_rules_closure", - sha256 = "b29a8bc2cb10513c864cb1084d6f38613ef14a143797cea0af0f91cd385f5e8c", - strip_prefix = "rules_closure-0.8.0", - urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/rules_closure/archive/0.8.0.tar.gz", - "https://github.com/bazelbuild/rules_closure/archive/0.8.0.tar.gz", - ], -) -load("@io_bazel_rules_closure//closure:defs.bzl", "closure_repositories") - -closure_repositories( - omit_com_google_protobuf = True, -) - diff --git a/cast_receiver_app/app-desktop/html/index.css b/cast_receiver_app/app-desktop/html/index.css deleted file mode 100644 index ff77e1cbfa..0000000000 --- a/cast_receiver_app/app-desktop/html/index.css +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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. - */ -html, body, section, video, div, span, ul, li { - border: 0; - box-sizing: border-box; - margin: 0; - padding: 0; -} -body, html { - height: 100%; - overflow: auto; - background-color: #333; - color: #eeeeee; - font-family: Roboto, Arial, sans-serif; -} -body { - padding-top: 24px; -} -.exo_controls { - list-style: none; - padding: 0; - white-space: nowrap; - margin-top: 12px; -} -.exo_controls > li { - display: inline-block; - width: 72px; -} -.exo_controls > .large { - width: 140px; -} -/* an action element to add or remove a media item */ -.action { - margin: 4px auto; - max-width: 640px; -} -.action.prepared { - background-color: #AA0000; -} -/** marks whether a given media item is in the queue */ -.queue-marker { - background-color: #AA0000; - border-radius: 50%; - border: 1px solid #ffc0c0; - display: none; - float: right; - height: 1em; - margin-top: 1px; - width: 1em; -} -.action[data-uuid] .queue-marker { - display: inline-block; -} -.action.prepared .queue-marker { - background-color: #fff900; -} -.playing .action.prepared .queue-marker { - animation-name: spin; - animation-iteration-count: infinite; - animation-duration: 1.6s; -} -/* A simple button. */ -.button { - background-color: #45484d; - border: 1px solid #495267; - border-radius: 3px; - color: #FFFFFF; - cursor: pointer; - font-size: 12px; - font-weight: bold; - padding: 10px 10px 10px 10px; - text-decoration: none; - text-shadow: -1px -1px 0 rgba(0,0,0,0.3); - -webkit-user-select: none; -} -.button:hover { - border: 1px solid #363d4c; - background-color: #2d2f32; - background-image: linear-gradient(to bottom, #2d2f32, #1a1a1a); -} -.ribbon { - background-color: #003a5dc2; - box-shadow: 2px 2px 4px #000; - left: -60px; - height: 3.3em; - padding-top: 7px; - position: absolute; - text-align: center; - top: 27px; - transform: rotateZ(-45deg); - width: 220px; - border: 1px dashed #cacaca; - outline-color: #003a5dc2; - outline-width: 2px; - outline-style: solid; -} -.ribbon a { - color: white; - text-decoration: none; - -webkit-user-select: none; -} -#button_prepare { - left: 0; - position: absolute; -} -#button_stop { - position: absolute; - right: 0; -} -#exo_demo_view { - height: 360px; - margin: auto; - overflow: hidden; - position: relative; - width: 640px; -} -#video { - background-color: #000; - border-radius: 8px; - height: 100%; - margin-bottom: auto; - margin-top: auto; - width: 100%; -} -#exo_controls { - display: none; - margin: auto; - position: relative; - text-align: center; - width: 640px; -} -#media-actions { - margin-top: 12px; -} - -@keyframes spin { - from { - transform: rotateX(0deg); - } - to { - transform: rotateX(180deg); - } -} diff --git a/cast_receiver_app/app-desktop/html/index.html b/cast_receiver_app/app-desktop/html/index.html deleted file mode 100644 index 19a118913b..0000000000 --- a/cast_receiver_app/app-desktop/html/index.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - -

        -
        - -
        -
        -
        -
        -
        - - -
        -
        -
        - for debugging
        purpose only -
        -
        -
        -
          -
        • prepare
        • -
        • prev
        • -
        • rewind
        • -
        • play
        • -
        • pause
        • -
        • ffwd
        • -
        • next
        • -
        • stop
        • -
        -
        -
        -
        - - - diff --git a/cast_receiver_app/app-desktop/src/main.js b/cast_receiver_app/app-desktop/src/main.js deleted file mode 100644 index 5645d70787..0000000000 --- a/cast_receiver_app/app-desktop/src/main.js +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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. - */ - -goog.module('exoplayer.cast.debug'); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const PlaybackInfoView = goog.require('exoplayer.cast.PlaybackInfoView'); -const Player = goog.require('exoplayer.cast.Player'); -const PlayerControls = goog.require('exoplayer.cast.PlayerControls'); -const ShakaPlayer = goog.require('shaka.Player'); -const SimpleTextDisplayer = goog.require('shaka.text.SimpleTextDisplayer'); -const installAll = goog.require('shaka.polyfill.installAll'); -const util = goog.require('exoplayer.cast.util'); - -/** @type {!Array} */ -let queue = []; -/** @type {number} */ -let uuidCounter = 1; - -// install all polyfills for the Shaka player -installAll(); - -/** - * Listens for player state changes and logs the state to the console. - * - * @param {!PlayerState} playerState The player state. - */ -const playerListener = function(playerState) { - util.log(['playerState: ', playerState.playbackPosition, playerState]); - queue = playerState.mediaQueue; - highlightCurrentItem( - playerState.playbackPosition && playerState.playbackPosition.uuid ? - playerState.playbackPosition.uuid : - ''); - if (playerState.playWhenReady && playerState.playbackState === 'READY') { - document.body.classList.add('playing'); - } else { - document.body.classList.remove('playing'); - } - if (playerState.playbackState === 'IDLE' && queue.length === 0) { - // Stop has been called or player not yet prepared. - resetSampleList(); - } -}; - -/** - * Highlights the currently playing item in the samples list. - * - * @param {string} uuid - */ -const highlightCurrentItem = function(uuid) { - const actions = /** @type {!NodeList} */ ( - document.querySelectorAll('#media-actions .action')); - for (let action of actions) { - if (action.dataset['uuid'] === uuid) { - action.classList.add('prepared'); - } else { - action.classList.remove('prepared'); - } - } -}; - -/** - * Makes sure all items reflect being removed from the timeline. - */ -const resetSampleList = function() { - const actions = /** @type {!NodeList} */ ( - document.querySelectorAll('#media-actions .action')); - for (let action of actions) { - action.classList.remove('prepared'); - delete action.dataset['uuid']; - } -}; - -/** - * If the arguments provide a valid media item it is added to the player. - * - * @param {!MediaItem} item The media item. - * @return {string} The uuid which has been created for the item before adding. - */ -const addQueueItem = function(item) { - if (!(item.media && item.media.uri && item.mimeType)) { - throw Error('insufficient arguments to add a queue item'); - } - item.uuid = 'uuid-' + uuidCounter++; - player.addQueueItems(queue.length, [item], /* playbackOrder= */ undefined); - return item.uuid; -}; - -/** - * An event listener which listens for actions. - * - * @param {!Event} ev The DOM event. - */ -const handleAction = (ev) => { - let target = ev.target; - while (target !== document.body && !target.dataset['action']) { - target = target.parentNode; - } - if (!target || !target.dataset['action']) { - return; - } - switch (target.dataset['action']) { - case 'player.addItems': - if (target.dataset['uuid']) { - player.removeQueueItems([target.dataset['uuid']]); - delete target.dataset['uuid']; - } else { - const uuid = addQueueItem(/** @type {!MediaItem} */ - (JSON.parse(target.dataset['item']))); - target.dataset['uuid'] = uuid; - } - break; - } -}; - -/** - * Appends samples to the list of media item actions. - * - * @param {!Array} mediaItems The samples to add. - */ -const appendSamples = function(mediaItems) { - const samplesList = document.getElementById('media-actions'); - mediaItems.forEach((item) => { - const div = /** @type {!HTMLElement} */ (document.createElement('div')); - div.classList.add('action', 'button'); - div.dataset['action'] = 'player.addItems'; - div.dataset['item'] = JSON.stringify(item); - div.appendChild(document.createTextNode(item.title)); - const marker = document.createElement('span'); - marker.classList.add('queue-marker'); - div.appendChild(marker); - samplesList.appendChild(div); - }); -}; - -/** @type {!HTMLMediaElement} */ -const mediaElement = - /** @type {!HTMLMediaElement} */ (document.getElementById('video')); -// Workaround for https://github.com/google/shaka-player/issues/1819 -// TODO(bachinger) Remove line when better fix available. -new SimpleTextDisplayer(mediaElement); -/** @type {!ShakaPlayer} */ -const shakaPlayer = new ShakaPlayer(mediaElement); -/** @type {!Player} */ -const player = new Player(shakaPlayer, new ConfigurationFactory()); -new PlayerControls(player, 'exo_controls'); -new PlaybackInfoView(player, 'exo_playback_info'); - -// register listeners -document.body.addEventListener('click', handleAction); -player.addPlayerListener(playerListener); - -// expose the player for debugging purposes. -window['player'] = player; - -exports.appendSamples = appendSamples; diff --git a/cast_receiver_app/app-desktop/src/player_controls.js b/cast_receiver_app/app-desktop/src/player_controls.js deleted file mode 100644 index e29f74148c..0000000000 --- a/cast_receiver_app/app-desktop/src/player_controls.js +++ /dev/null @@ -1,164 +0,0 @@ -/* - * 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. - */ -goog.module('exoplayer.cast.PlayerControls'); - -const Player = goog.require('exoplayer.cast.Player'); - -/** - * A simple UI to control the player. - * - */ -class PlayerControls { - /** - * @param {!Player} player The player. - * @param {string} containerId The id of the container element. - */ - constructor(player, containerId) { - /** @const @private {!Player} */ - this.player_ = player; - /** @const @private {?Element} */ - this.root_ = document.getElementById(containerId); - /** @const @private {?Element} */ - this.playButton_ = this.root_.querySelector('#button_play'); - /** @const @private {?Element} */ - this.pauseButton_ = this.root_.querySelector('#button_pause'); - /** @const @private {?Element} */ - this.previousButton_ = this.root_.querySelector('#button_previous'); - /** @const @private {?Element} */ - this.nextButton_ = this.root_.querySelector('#button_next'); - - const previous = () => { - const index = player.getPreviousWindowIndex(); - if (index !== -1) { - player.seekToWindow(index, 0); - } - }; - const next = () => { - const index = player.getNextWindowIndex(); - if (index !== -1) { - player.seekToWindow(index, 0); - } - }; - const rewind = () => { - player.seekToWindow( - player.getCurrentWindowIndex(), - player.getCurrentPositionMs() - 15000); - }; - const fastForward = () => { - player.seekToWindow( - player.getCurrentWindowIndex(), - player.getCurrentPositionMs() + 30000); - }; - const actions = { - 'pwr_1': (ev) => player.setPlayWhenReady(true), - 'pwr_0': (ev) => player.setPlayWhenReady(false), - 'rewind': rewind, - 'fastforward': fastForward, - 'previous': previous, - 'next': next, - 'prepare': (ev) => player.prepare(), - 'stop': (ev) => player.stop(true), - 'remove_queue_item': (ev) => { - player.removeQueueItems([ev.target.dataset.id]); - }, - }; - /** - * @param {!Event} ev The key event. - * @return {boolean} true if the key event has been handled. - */ - const keyListener = (ev) => { - const key = /** @type {!KeyboardEvent} */ (ev).key; - switch (key) { - case 'ArrowUp': - case 'k': - previous(); - ev.preventDefault(); - return true; - case 'ArrowDown': - case 'j': - next(); - ev.preventDefault(); - return true; - case 'ArrowLeft': - case 'h': - rewind(); - ev.preventDefault(); - return true; - case 'ArrowRight': - case 'l': - fastForward(); - ev.preventDefault(); - return true; - case ' ': - case 'p': - player.setPlayWhenReady(!player.getPlayWhenReady()); - ev.preventDefault(); - return true; - } - return false; - }; - document.addEventListener('keydown', keyListener); - this.root_.addEventListener('click', function(ev) { - const method = ev.target['dataset']['method']; - if (actions[method]) { - actions[method](ev); - } - return true; - }); - player.addPlayerListener((playerState) => this.updateUi(playerState)); - player.invalidate(); - this.setVisible_(true); - } - - /** - * Syncs the ui with the player state. - * - * @param {!PlayerState} playerState The state of the player to be reflected - * by the UI. - */ - updateUi(playerState) { - if (playerState.playWhenReady) { - this.playButton_.style.display = 'none'; - this.pauseButton_.style.display = 'inline-block'; - } else { - this.playButton_.style.display = 'inline-block'; - this.pauseButton_.style.display = 'none'; - } - if (this.player_.getNextWindowIndex() === -1) { - this.nextButton_.style.visibility = 'hidden'; - } else { - this.nextButton_.style.visibility = 'visible'; - } - if (this.player_.getPreviousWindowIndex() === -1) { - this.previousButton_.style.visibility = 'hidden'; - } else { - this.previousButton_.style.visibility = 'visible'; - } - } - - /** - * @private - * @param {boolean} visible If `true` thie controls are shown. If `false` the - * controls are hidden. - */ - setVisible_(visible) { - if (this.root_) { - this.root_.style.display = visible ? 'block' : 'none'; - } - } -} - -exports = PlayerControls; diff --git a/cast_receiver_app/app-desktop/src/samples.js b/cast_receiver_app/app-desktop/src/samples.js deleted file mode 100644 index 2d190bdef4..0000000000 --- a/cast_receiver_app/app-desktop/src/samples.js +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ -goog.module('exoplayer.cast.samples'); - -const {appendSamples} = goog.require('exoplayer.cast.debug'); - -appendSamples([ - { - title: 'DASH: multi-period', - mimeType: 'application/dash+xml', - media: { - uri: 'https://storage.googleapis.com/exoplayer-test-media-internal-6383' + - '4241aced7884c2544af1a3452e01/dash/multi-period/two-periods-minimal' + - '-duration.mpd', - }, - }, - { - title: 'HLS: Angel one', - mimeType: 'application/vnd.apple.mpegurl', - media: { - uri: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hl' + - 's.m3u8', - }, - }, - { - title: 'MP4: Elephants dream', - mimeType: 'video/*', - media: { - uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/' + - 'ElephantsDream.mp4', - }, - }, - { - title: 'MKV: Android screens', - mimeType: 'video/*', - media: { - uri: 'https://storage.googleapis.com/exoplayer-test-media-1/mkv/android' + - '-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv', - }, - }, - { - title: 'WV: HDCP not specified', - mimeType: 'application/dash+xml', - media: { - uri: 'https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd', - }, - drmSchemes: [ - { - licenseServer: { - uri: 'https://proxy.uat.widevine.com/proxy?video_id=d286538032258a1' + - 'c&provider=widevine_test', - }, - uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', - }, - ], - }, -]); diff --git a/cast_receiver_app/app-desktop/src/samples_internal.js b/cast_receiver_app/app-desktop/src/samples_internal.js deleted file mode 100644 index 71b05eb2c1..0000000000 --- a/cast_receiver_app/app-desktop/src/samples_internal.js +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ -goog.module('exoplayer.cast.samplesinternal'); - -const {appendSamples} = goog.require('exoplayer.cast.debug'); - -appendSamples([ - { - title: 'DAS: VOD', - mimeType: 'application/dash+xml', - media: { - uri: 'https://demo-dash-pvr.zahs.tv/hd/manifest.mpd', - }, - }, - { - title: 'MP3', - mimeType: 'audio/*', - media: { - uri: 'http://www.noiseaddicts.com/samples_1w72b820/4190.mp3', - }, - }, - { - title: 'DASH: live', - mimeType: 'application/dash+xml', - media: { - uri: 'https://demo-dash-live.zahs.tv/sd/manifest.mpd', - }, - }, - { - title: 'HLS: live', - mimeType: 'application/vnd.apple.mpegurl', - media: { - uri: 'https://demo-hls5-live.zahs.tv/sd/master.m3u8', - }, - }, - { - title: 'Live DASH (HD/Widevine)', - mimeType: 'application/dash+xml', - media: { - uri: 'https://demo-dashenc-live.zahs.tv/hd/widevine.mpd', - }, - drmSchemes: [ - { - licenseServer: { - uri: 'https://demo-dashenc-live.zahs.tv/hd/widevine-license', - }, - uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', - }, - ], - }, - { - title: 'VOD DASH (HD/Widevine)', - mimeType: 'application/dash+xml', - media: { - uri: 'https://demo-dashenc-pvr.zahs.tv/hd/widevine.mpd', - }, - drmSchemes: [ - { - licenseServer: { - uri: 'https://demo-dashenc-live.zahs.tv/hd/widevine-license', - }, - uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', - }, - ], - }, -]); diff --git a/cast_receiver_app/app/html/index.css b/cast_receiver_app/app/html/index.css deleted file mode 100644 index dfc9b4e0e5..0000000000 --- a/cast_receiver_app/app/html/index.css +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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. - */ -section, video, div, span, body, html { - border: 0; - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html, body { - background-color: #000; - height: 100%; - overflow: hidden; -} - -#exo_player_view { - background-color: #000; - height: 100%; - position: relative; -} - -#exo_video { - height: 100%; - width: 100%; -} - diff --git a/cast_receiver_app/app/html/index.html b/cast_receiver_app/app/html/index.html deleted file mode 100644 index 64de3e8a8e..0000000000 --- a/cast_receiver_app/app/html/index.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - -
        - -
        -
        -
        -
        -
        - - -
        -
        -
        - - - diff --git a/cast_receiver_app/app/html/playback_info_view.css b/cast_receiver_app/app/html/playback_info_view.css deleted file mode 100644 index f70695d873..0000000000 --- a/cast_receiver_app/app/html/playback_info_view.css +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -.exo_text_label { - color: #fff; - font-family: Roboto, Arial, sans-serif; - font-size: 1em; - margin-top: 4px; -} - -#exo_playback_info { - bottom: 5%; - display: none; - left: 4%; - position: absolute; - right: 4%; - width: 92%; -} - -#exo_time_bar { - width: 100%; -} - -#exo_duration { - background-color: rgba(255, 255, 255, 0.4); - height: 0.5em; - overflow: hidden; - position: relative; - width: 100%; -} - -#exo_elapsed_time { - background-color: rgb(73, 128, 218); - height: 100%; - opacity: 1; - width: 0; -} - -#exo_duration_label { - float: right; -} - -#exo_elapsed_time_label { - float: left; -} - diff --git a/cast_receiver_app/app/src/main.js b/cast_receiver_app/app/src/main.js deleted file mode 100644 index 37c6fd41eb..0000000000 --- a/cast_receiver_app/app/src/main.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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. - */ - -goog.module('exoplayer.cast.app'); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher'); -const PlaybackInfoView = goog.require('exoplayer.cast.PlaybackInfoView'); -const Player = goog.require('exoplayer.cast.Player'); -const Receiver = goog.require('exoplayer.cast.Receiver'); -const ShakaPlayer = goog.require('shaka.Player'); -const SimpleTextDisplayer = goog.require('shaka.text.SimpleTextDisplayer'); -const installAll = goog.require('shaka.polyfill.installAll'); - -/** - * The ExoPlayer namespace for messages sent and received via cast message bus. - */ -const MESSAGE_NAMESPACE_EXOPLAYER = 'urn:x-cast:com.google.exoplayer.cast'; - -// installs all polyfills for the Shaka player -installAll(); -/** @type {?HTMLMediaElement} */ -const videoElement = - /** @type {?HTMLMediaElement} */ (document.getElementById('exo_video')); -if (videoElement !== null) { - // Workaround for https://github.com/google/shaka-player/issues/1819 - // TODO(bachinger) Remove line when better fix available. - new SimpleTextDisplayer(videoElement); - /** @type {!cast.framework.CastReceiverContext} */ - const castReceiverContext = cast.framework.CastReceiverContext.getInstance(); - const shakaPlayer = new ShakaPlayer(/** @type {!HTMLMediaElement} */ - (videoElement)); - const player = new Player(shakaPlayer, new ConfigurationFactory()); - new PlaybackInfoView(player, 'exo_playback_info'); - if (castReceiverContext !== null) { - const messageDispatcher = - new MessageDispatcher(MESSAGE_NAMESPACE_EXOPLAYER, castReceiverContext); - new Receiver(player, castReceiverContext, messageDispatcher); - } - // expose player for debugging purposes. - window['player'] = player; -} diff --git a/cast_receiver_app/app/src/message_dispatcher.js b/cast_receiver_app/app/src/message_dispatcher.js deleted file mode 100644 index 151ac87fbe..0000000000 --- a/cast_receiver_app/app/src/message_dispatcher.js +++ /dev/null @@ -1,234 +0,0 @@ -/* - * 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. - */ -goog.module('exoplayer.cast.MessageDispatcher'); - -const validation = goog.require('exoplayer.cast.validation'); - -/** - * A callback function which is called by an action handler to indicate when - * processing has completed. - * - * @typedef {function(?PlayerState): undefined} - */ -const Callback = undefined; - -/** - * Handles an action sent by a sender app. - * - * @typedef {function(!Object, number, string, !Callback): undefined} - */ -const ActionHandler = undefined; - -/** - * Dispatches messages of a cast message bus to registered action handlers. - * - *

        The dispatcher listens to events of a CastMessageBus for the namespace - * passed to the constructor. The data property of the event is - * parsed as a json document and delegated to a handler registered for the given - * method. - */ -class MessageDispatcher { - /** - * @param {string} namespace The message namespace. - * @param {!cast.framework.CastReceiverContext} castReceiverContext The cast - * receiver manager. - */ - constructor(namespace, castReceiverContext) { - /** @private @const {string} */ - this.namespace_ = namespace; - /** @private @const {!cast.framework.CastReceiverContext} */ - this.castReceiverContext_ = castReceiverContext; - /** @private @const {!Array} */ - this.messageQueue_ = []; - /** @private @const {!Object} */ - this.actions_ = {}; - /** @private @const {!Object} */ - this.senderSequences_ = {}; - /** @private @const {function(string, *)} */ - this.jsonStringifyReplacer_ = (key, value) => { - if (value === Infinity || value === null) { - return undefined; - } - return value; - }; - this.castReceiverContext_.addCustomMessageListener( - this.namespace_, this.onMessage.bind(this)); - } - - /** - * Registers a handler of a given action. - * - * @param {string} method The method name for which to register the handler. - * @param {!Array>} argDefs The name and type of each argument - * or an empty array if the method has no arguments. - * @param {!ActionHandler} handler A function to process the action. - */ - registerActionHandler(method, argDefs, handler) { - this.actions_[method] = { - method, - argDefs, - handler, - }; - } - - /** - * Unregisters the handler of the given action. - * - * @param {string} action The action to unregister. - */ - unregisterActionHandler(action) { - delete this.actions_[action]; - } - - /** - * Callback to receive messages sent by sender apps. - * - * @param {!cast.framework.system.Event} event The event received from the - * sender app. - */ - onMessage(event) { - console.log('message arrived from sender', this.namespace_, event); - const message = /** @type {!ExoCastMessage} */ (event.data); - const action = this.actions_[message.method]; - if (action) { - const args = message.args; - for (let i = 0; i < action.argDefs.length; i++) { - if (!validation.validateProperty( - args, action.argDefs[i][0], action.argDefs[i][1])) { - console.warn('invalid method call', message); - return; - } - } - this.messageQueue_.push({ - senderId: event.senderId, - message: message, - handler: action.handler - }); - if (this.messageQueue_.length === 1) { - this.executeNext(); - } else { - // Do nothing. An action is executing asynchronously and will call - // executeNext when finished. - } - } else { - console.warn('handler of method not found', message); - } - } - - /** - * Executes the next message in the queue. - */ - executeNext() { - if (this.messageQueue_.length === 0) { - return; - } - const head = this.messageQueue_[0]; - const message = head.message; - const senderSequence = message.sequenceNumber; - this.senderSequences_[head.senderId] = senderSequence; - try { - head.handler(message.args, senderSequence, head.senderId, (response) => { - if (response) { - this.send(head.senderId, response); - } - this.shiftPendingMessage_(head); - }); - } catch (e) { - this.shiftPendingMessage_(head); - console.error('error while executing method : ' + message.method, e); - } - } - - /** - * Broadcasts the sender state to all sender apps registered for the - * given message namespace. - * - * @param {!PlayerState} playerState The player state to be sent. - */ - broadcast(playerState) { - this.castReceiverContext_.getSenders().forEach((sender) => { - this.send(sender.id, playerState); - }); - delete playerState.sequenceNumber; - } - - /** - * Sends the PlayerState to the given sender. - * - * @param {string} senderId The id of the sender. - * @param {!PlayerState} playerState The message to send. - */ - send(senderId, playerState) { - playerState.sequenceNumber = this.senderSequences_[senderId] || -1; - this.castReceiverContext_.sendCustomMessage( - this.namespace_, senderId, - // TODO(bachinger) Find a better solution. - JSON.parse(JSON.stringify(playerState, this.jsonStringifyReplacer_))); - } - - /** - * Notifies the message dispatcher that a given sender has disconnected from - * the receiver. - * - * @param {string} senderId The id of the sender. - */ - notifySenderDisconnected(senderId) { - delete this.senderSequences_[senderId]; - } - - /** - * Shifts the pending message and executes the next if any. - * - * @private - * @param {!Message} pendingMessage The pending message. - */ - shiftPendingMessage_(pendingMessage) { - if (pendingMessage === this.messageQueue_[0]) { - this.messageQueue_.shift(); - this.executeNext(); - } - } -} - -/** - * An item in the message queue. - * - * @record - */ -function Message() {} - -/** - * The sender id. - * - * @type {string} - */ -Message.prototype.senderId; - -/** - * The ExoCastMessage sent by the sender app. - * - * @type {!ExoCastMessage} - */ -Message.prototype.message; - -/** - * The handler function handling the message. - * - * @type {!ActionHandler} - */ -Message.prototype.handler; - -exports = MessageDispatcher; diff --git a/cast_receiver_app/app/src/receiver.js b/cast_receiver_app/app/src/receiver.js deleted file mode 100644 index 5e67219e75..0000000000 --- a/cast_receiver_app/app/src/receiver.js +++ /dev/null @@ -1,191 +0,0 @@ -/** - * 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. - */ - -goog.module('exoplayer.cast.Receiver'); - -const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher'); -const Player = goog.require('exoplayer.cast.Player'); -const validation = goog.require('exoplayer.cast.validation'); - -/** - * The Receiver receives messages from a message bus and delegates to - * the player. - * - * @constructor - * @param {!Player} player The player. - * @param {!cast.framework.CastReceiverContext} context The cast receiver - * context. - * @param {!MessageDispatcher} messageDispatcher The message dispatcher to use. - */ -const Receiver = function(player, context, messageDispatcher) { - addPlayerActions(messageDispatcher, player); - addQueueActions(messageDispatcher, player); - player.addPlayerListener((playerState) => { - messageDispatcher.broadcast(playerState); - }); - - context.addEventListener( - cast.framework.system.EventType.SENDER_CONNECTED, (event) => { - messageDispatcher.send(event.senderId, player.getPlayerState()); - }); - - context.addEventListener( - cast.framework.system.EventType.SENDER_DISCONNECTED, (event) => { - messageDispatcher.notifySenderDisconnected(event.senderId); - if (event.reason === - cast.framework.system.DisconnectReason.REQUESTED_BY_SENDER && - context.getSenders().length === 0) { - window.close(); - } - }); - - // Start the cast receiver context. - context.start(); -}; - -/** - * Registers action handlers for playback messages sent by the sender app. - * - * @param {!MessageDispatcher} messageDispatcher The dispatcher. - * @param {!Player} player The player. - */ -const addPlayerActions = function(messageDispatcher, player) { - messageDispatcher.registerActionHandler( - 'player.setPlayWhenReady', [['playWhenReady', 'boolean']], - (args, senderSequence, senderId, callback) => { - const playWhenReady = args['playWhenReady']; - callback( - !player.setPlayWhenReady(playWhenReady) ? - player.getPlayerState() : - null); - }); - messageDispatcher.registerActionHandler( - 'player.seekTo', - [ - ['uuid', 'string'], - ['positionMs', '?number'], - ], - (args, senderSequence, senderId, callback) => { - callback( - !player.seekToUuid(args['uuid'], args['positionMs']) ? - player.getPlayerState() : - null); - }); - messageDispatcher.registerActionHandler( - 'player.setRepeatMode', [['repeatMode', 'RepeatMode']], - (args, senderSequence, senderId, callback) => { - callback( - !player.setRepeatMode(args['repeatMode']) ? - player.getPlayerState() : - null); - }); - messageDispatcher.registerActionHandler( - 'player.setShuffleModeEnabled', [['shuffleModeEnabled', 'boolean']], - (args, senderSequence, senderId, callback) => { - callback( - !player.setShuffleModeEnabled(args['shuffleModeEnabled']) ? - player.getPlayerState() : - null); - }); - messageDispatcher.registerActionHandler( - 'player.onClientConnected', [], - (args, senderSequence, senderId, callback) => { - callback(player.getPlayerState()); - }); - messageDispatcher.registerActionHandler( - 'player.stop', [['reset', 'boolean']], - (args, senderSequence, senderId, callback) => { - player.stop(args['reset']).then(() => { - callback(null); - }); - }); - messageDispatcher.registerActionHandler( - 'player.prepare', [], (args, senderSequence, senderId, callback) => { - player.prepare(); - callback(null); - }); - messageDispatcher.registerActionHandler( - 'player.setTrackSelectionParameters', - [ - ['preferredAudioLanguage', 'string'], - ['preferredTextLanguage', 'string'], - ['disabledTextTrackSelectionFlags', 'Array'], - ['selectUndeterminedTextLanguage', 'boolean'], - ], - (args, senderSequence, senderId, callback) => { - const trackSelectionParameters = - /** @type {!TrackSelectionParameters} */ ({ - preferredAudioLanguage: args['preferredAudioLanguage'], - preferredTextLanguage: args['preferredTextLanguage'], - disabledTextTrackSelectionFlags: - args['disabledTextTrackSelectionFlags'], - selectUndeterminedTextLanguage: - args['selectUndeterminedTextLanguage'], - }); - callback( - !player.setTrackSelectionParameters(trackSelectionParameters) ? - player.getPlayerState() : - null); - }); -}; - -/** - * Registers action handlers for queue management messages sent by the sender - * app. - * - * @param {!MessageDispatcher} messageDispatcher The dispatcher. - * @param {!Player} player The player. - */ -const addQueueActions = - function (messageDispatcher, player) { - messageDispatcher.registerActionHandler( - 'player.addItems', - [ - ['index', '?number'], - ['items', 'Array'], - ['shuffleOrder', 'Array'], - ], - (args, senderSequence, senderId, callback) => { - const mediaItems = args['items']; - const index = args['index'] || player.getQueueSize(); - let addedItemCount; - if (validation.validateMediaItems(mediaItems)) { - addedItemCount = - player.addQueueItems(index, mediaItems, args['shuffleOrder']); - } - callback(addedItemCount === 0 ? player.getPlayerState() : null); - }); - messageDispatcher.registerActionHandler( - 'player.removeItems', [['uuids', 'Array']], - (args, senderSequence, senderId, callback) => { - const removedItemsCount = player.removeQueueItems(args['uuids']); - callback(removedItemsCount === 0 ? player.getPlayerState() : null); - }); - messageDispatcher.registerActionHandler( - 'player.moveItem', - [ - ['uuid', 'string'], - ['index', 'number'], - ['shuffleOrder', 'Array'], - ], - (args, senderSequence, senderId, callback) => { - const hasMoved = player.moveQueueItem( - args['uuid'], args['index'], args['shuffleOrder']); - callback(!hasMoved ? player.getPlayerState() : null); - }); -}; - -exports = Receiver; diff --git a/cast_receiver_app/app/src/validation.js b/cast_receiver_app/app/src/validation.js deleted file mode 100644 index 23e2708f8e..0000000000 --- a/cast_receiver_app/app/src/validation.js +++ /dev/null @@ -1,163 +0,0 @@ -/** - * 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. - * - * @fileoverview A validator for messages received from sender apps. - */ - -goog.module('exoplayer.cast.validation'); - -const {getPlaybackType, PlaybackType, RepeatMode} = goog.require('exoplayer.cast.constants'); - -/** - * Media item fields. - * - * @enum {string} - */ -const MediaItemField = { - UUID: 'uuid', - MEDIA: 'media', - MIME_TYPE: 'mimeType', - DRM_SCHEMES: 'drmSchemes', - TITLE: 'title', - DESCRIPTION: 'description', - START_POSITION_US: 'startPositionUs', - END_POSITION_US: 'endPositionUs', -}; - -/** - * DrmScheme fields. - * - * @enum {string} - */ -const DrmSchemeField = { - UUID: 'uuid', - LICENSE_SERVER_URI: 'licenseServer', -}; - -/** - * UriBundle fields. - * - * @enum {string} - */ -const UriBundleField = { - URI: 'uri', - REQUEST_HEADERS: 'requestHeaders', -}; - -/** - * Validates an array of media items. - * - * @param {!Array} mediaItems An array of media items. - * @return {boolean} true if all media items are valid, otherwise false is - * returned. - */ -const validateMediaItems = function (mediaItems) { - for (let i = 0; i < mediaItems.length; i++) { - if (!validateMediaItem(mediaItems[i])) { - return false; - } - } - return true; -}; - -/** - * Validates a queue item sent to the receiver by a sender app. - * - * @param {!MediaItem} mediaItem The media item. - * @return {boolean} true if the media item is valid, false otherwise. - */ -const validateMediaItem = function (mediaItem) { - // validate minimal properties - if (!validateProperty(mediaItem, MediaItemField.UUID, 'string')) { - console.log('missing mandatory uuid', mediaItem.uuid); - return false; - } - if (!validateProperty(mediaItem.media, UriBundleField.URI, 'string')) { - console.log('missing mandatory', mediaItem.media ? 'uri' : 'media'); - return false; - } - const mimeType = mediaItem.mimeType; - if (!mimeType || getPlaybackType(mimeType) === PlaybackType.UNKNOWN) { - console.log('unsupported mime type:', mimeType); - return false; - } - // validate optional properties - if (goog.isArray(mediaItem.drmSchemes)) { - for (let i = 0; i < mediaItem.drmSchemes.length; i++) { - let drmScheme = mediaItem.drmSchemes[i]; - if (!validateProperty(drmScheme, DrmSchemeField.UUID, 'string') || - !validateProperty( - drmScheme.licenseServer, UriBundleField.URI, 'string')) { - console.log('invalid drm scheme', drmScheme); - return false; - } - } - } - if (!validateProperty(mediaItem, MediaItemField.START_POSITION_US, '?number') - || !validateProperty(mediaItem, MediaItemField.END_POSITION_US, '?number') - || !validateProperty(mediaItem, MediaItemField.TITLE, '?string') - || !validateProperty(mediaItem, MediaItemField.DESCRIPTION, '?string')) { - console.log('invalid type of one of startPositionUs, endPositionUs, title' - + ' or description', mediaItem); - return false; - } - return true; -}; - -/** - * Validates the existence and type of a property. - * - *

        Supported types: number, string, boolean, Array. - *

        Prefix the type with a ? to indicate that the property is optional. - * - * @param {?Object|?MediaItem|?UriBundle} obj The object to validate. - * @param {string} propertyName The name of the property. - * @param {string} type The type of the property. - * @return {boolean} True if valid, false otherwise. - */ -const validateProperty = function (obj, propertyName, type) { - if (typeof obj === 'undefined' || obj === null) { - return false; - } - const isOptional = type.startsWith('?'); - const value = obj[propertyName]; - if (isOptional && typeof value === 'undefined') { - return true; - } - type = isOptional ? type.substring(1) : type; - switch (type) { - case 'string': - return typeof value === 'string' || value instanceof String; - case 'number': - return typeof value === 'number' && isFinite(value); - case 'Array': - return typeof value !== 'undefined' && typeof value === 'object' - && value.constructor === Array; - case 'boolean': - return typeof value === 'boolean'; - case 'RepeatMode': - return value === RepeatMode.OFF || value === RepeatMode.ONE || - value === RepeatMode.ALL; - default: - console.warn('Unsupported type when validating an object property. ' + - 'Supported types are string, number, boolean and Array.', type); - return false; - } -}; - -exports.validateMediaItem = validateMediaItem; -exports.validateMediaItems = validateMediaItems; -exports.validateProperty = validateProperty; - diff --git a/cast_receiver_app/assemble.bazel.sh b/cast_receiver_app/assemble.bazel.sh deleted file mode 100755 index d2039a5152..0000000000 --- a/cast_receiver_app/assemble.bazel.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/bin/bash -# Copyright (C) 2019 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. - -## -# Assembles the html, css and javascript files which have been created by the -# bazel build in a destination directory. - -HTML_DIR=app/html -HTML_DEBUG_DIR=app-desktop/html -BIN=bazel-bin - -function usage { - echo "usage: `basename "$0"` -d=DESTINATION_DIR" -} - -for i in "$@" -do -case $i in - -d=*|--destination=*) - DESTINATION="${i#*=}" - shift # past argument=value - ;; - -h|--help) - usage - exit 0 - ;; - *) - # unknown option - ;; -esac -done - -if [ ! -d "$DESTINATION" ]; then - echo "destination directory '$DESTINATION' is not declared or is not a\ - directory" - usage - exit 1 -fi - -if [ ! -f "$BIN/app.js" ];then - echo "file $BIN/app.js not found. Did you build already with bazel?" - echo "-> # bazel build .. --incompatible_package_name_is_a_function=false" - exit 1 -fi - -if [ ! -f "$BIN/app_desktop.js" ];then - echo "file $BIN/app_desktop.js not found. Did you build already with bazel?" - echo "-> # bazel build .. --incompatible_package_name_is_a_function=false" - exit 1 -fi - -echo "assembling receiver and desktop app in $DESTINATION" -echo "-------" - -# cleaning up asset files in destination directory -FILES=( - app.js - app_desktop.js - app_styles.css - app_desktop_styles.css - index.html - player.html -) -for file in ${FILES[@]}; do - if [ -f $DESTINATION/$file ]; then - echo "deleting $file" - rm -f $DESTINATION/$file - fi -done -echo "-------" - -echo "copy html files to $DESTINATION" -cp $HTML_DIR/index.html $DESTINATION -cp $HTML_DEBUG_DIR/index.html $DESTINATION/player.html -echo "copy javascript files to $DESTINATION" -cp $BIN/app.js $BIN/app_desktop.js $DESTINATION -echo "copy css style to $DESTINATION" -cp $BIN/app_styles.css $BIN/app_desktop_styles.css $DESTINATION -echo "-------" - -echo "done." diff --git a/cast_receiver_app/externs/protocol.js b/cast_receiver_app/externs/protocol.js deleted file mode 100644 index d6544a6f37..0000000000 --- a/cast_receiver_app/externs/protocol.js +++ /dev/null @@ -1,489 +0,0 @@ -/* - * 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. - */ - -/** - * @fileoverview Externs for messages sent by a sender app in JSON format. - * - * Fields defined here are prevented from being renamed by the js compiler. - * - * @externs - */ - -/** - * An uri bundle with an uri and request parameters. - * - * @record - */ -class UriBundle { - constructor() { - /** - * The URI. - * - * @type {string} - */ - this.uri; - - /** - * The request headers. - * - * @type {?Object} - */ - this.requestHeaders; - } -} - -/** - * @record - */ -class DrmScheme { - constructor() { - /** - * The DRM UUID. - * - * @type {string} - */ - this.uuid; - - /** - * The license URI. - * - * @type {?UriBundle} - */ - this.licenseServer; - } -} - -/** - * @record - */ -class MediaItem { - constructor() { - /** - * The uuid of the item. - * - * @type {string} - */ - this.uuid; - - /** - * The mime type. - * - * @type {string} - */ - this.mimeType; - - /** - * The media uri bundle. - * - * @type {!UriBundle} - */ - this.media; - - /** - * The DRM schemes. - * - * @type {!Array} - */ - this.drmSchemes; - - /** - * The position to start playback from. - * - * @type {number} - */ - this.startPositionUs; - - /** - * The position at which to end playback. - * - * @type {number} - */ - this.endPositionUs; - - /** - * The title of the media item. - * - * @type {string} - */ - this.title; - - /** - * The description of the media item. - * - * @type {string} - */ - this.description; - } -} - -/** - * Constraint parameters for track selection. - * - * @record - */ -class TrackSelectionParameters { - constructor() { - /** - * The preferred audio language. - * - * @type {string|undefined} - */ - this.preferredAudioLanguage; - - /** - * The preferred text language. - * - * @type {string|undefined} - */ - this.preferredTextLanguage; - - /** - * List of selection flags that are disabled for text track selections. - * - * @type {!Array} - */ - this.disabledTextTrackSelectionFlags; - - /** - * Whether a text track with undetermined language should be selected if no - * track with `preferredTextLanguage` is available, or if - * `preferredTextLanguage` is unset. - * - * @type {boolean} - */ - this.selectUndeterminedTextLanguage; - } -} - -/** - * The PlaybackPosition defined by the position, the uuid of the media item and - * the period id. - * - * @record - */ -class PlaybackPosition { - constructor() { - /** - * The current playback position in milliseconds. - * - * @type {number} - */ - this.positionMs; - - /** - * The uuid of the media item. - * - * @type {string} - */ - this.uuid; - - /** - * The id of the currently playing period. - * - * @type {string} - */ - this.periodId; - - /** - * The reason of a position discontinuity if any. - * - * @type {?string} - */ - this.discontinuityReason; - } -} - -/** - * The playback parameters. - * - * @record - */ -class PlaybackParameters { - constructor() { - /** - * The playback speed. - * - * @type {number} - */ - this.speed; - - /** - * The playback pitch. - * - * @type {number} - */ - this.pitch; - - /** - * Whether silence is skipped. - * - * @type {boolean} - */ - this.skipSilence; - } -} -/** - * The player state. - * - * @record - */ -class PlayerState { - constructor() { - /** - * The playback state. - * - * @type {string} - */ - this.playbackState; - - /** - * The playback parameters. - * - * @type {!PlaybackParameters} - */ - this.playbackParameters; - - /** - * Playback starts when ready if true. - * - * @type {boolean} - */ - this.playWhenReady; - - /** - * The current position within the media. - * - * @type {?PlaybackPosition} - */ - this.playbackPosition; - - /** - * The current window index. - * - * @type {number} - */ - this.windowIndex; - - /** - * The number of windows. - * - * @type {number} - */ - this.windowCount; - - /** - * The audio tracks. - * - * @type {!Array} - */ - this.audioTracks; - - /** - * The video tracks in case of adaptive media. - * - * @type {!Array>} - */ - this.videoTracks; - - /** - * The repeat mode. - * - * @type {string} - */ - this.repeatMode; - - /** - * Whether the shuffle mode is enabled. - * - * @type {boolean} - */ - this.shuffleModeEnabled; - - /** - * The playback order to use when shuffle mode is enabled. - * - * @type {!Array} - */ - this.shuffleOrder; - - /** - * The queue of media items. - * - * @type {!Array} - */ - this.mediaQueue; - - /** - * The media item info of the queue items if available. - * - * @type {!Object} - */ - this.mediaItemsInfo; - - /** - * The sequence number of the sender. - * - * @type {number} - */ - this.sequenceNumber; - - /** - * The player error. - * - * @type {?PlayerError} - */ - this.error; - } -} - -/** - * The error description. - * - * @record - */ -class PlayerError { - constructor() { - /** - * The error message. - * - * @type {string} - */ - this.message; - - /** - * The error code. - * - * @type {number} - */ - this.code; - - /** - * The error category. - * - * @type {number} - */ - this.category; - } -} - -/** - * A period. - * - * @record - */ -class Period { - constructor() { - /** - * The id of the period. Must be unique within a media item. - * - * @type {string} - */ - this.id; - - /** - * The duration of the period in microseconds. - * - * @type {number} - */ - this.durationUs; - } -} -/** - * Holds dynamic information for a MediaItem. - * - *

        Holds information related to preparation for a specific {@link MediaItem}. - * Unprepared items are associated with an {@link #EMPTY} info object until - * prepared. - * - * @record - */ -class MediaItemInfo { - constructor() { - /** - * The duration of the window in microseconds. - * - * @type {number} - */ - this.windowDurationUs; - - /** - * The default start position relative to the start of the window in - * microseconds. - * - * @type {number} - */ - this.defaultStartPositionUs; - - /** - * The periods conforming the media item. - * - * @type {!Array} - */ - this.periods; - - /** - * The position of the window in the first period in microseconds. - * - * @type {number} - */ - this.positionInFirstPeriodUs; - - /** - * Whether it is possible to seek within the window. - * - * @type {boolean} - */ - this.isSeekable; - - /** - * Whether the window may change when the timeline is updated. - * - * @type {boolean} - */ - this.isDynamic; - } -} - -/** - * The message envelope send by a sender app. - * - * @record - */ -class ExoCastMessage { - constructor() { - /** - * The clients message sequenec number. - * - * @type {number} - */ - this.sequenceNumber; - - /** - * The name of the method. - * - * @type {string} - */ - this.method; - - /** - * The arguments of the method. - * - * @type {!Object} - */ - this.args; - } -}; - diff --git a/cast_receiver_app/externs/shaka.js b/cast_receiver_app/externs/shaka.js deleted file mode 100644 index 0af36d7b8c..0000000000 --- a/cast_receiver_app/externs/shaka.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -/** - * @fileoverview Externs of the Shaka configuration. - * - * @externs - */ - -/** - * The drm configuration for the Shaka player. - * - * @record - */ -class DrmConfiguration { - constructor() { - /** - * A map of license servers with the UUID of the drm system as the key and the - * license uri as the value. - * - * @type {!Object} - */ - this.servers; - } -} - -/** - * The configuration of the Shaka player. - * - * @record - */ -class PlayerConfiguration { - constructor() { - /** - * The preferred audio language. - * - * @type {string} - */ - this.preferredAudioLanguage; - - /** - * The preferred text language. - * - * @type {string} - */ - this.preferredTextLanguage; - - /** - * The drm configuration. - * - * @type {?DrmConfiguration} - */ - this.drm; - } -} diff --git a/cast_receiver_app/src/configuration_factory.js b/cast_receiver_app/src/configuration_factory.js deleted file mode 100644 index 819e52a755..0000000000 --- a/cast_receiver_app/src/configuration_factory.js +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -goog.module('exoplayer.cast.ConfigurationFactory'); - -const {DRM_SYSTEMS} = goog.require('exoplayer.cast.constants'); - -const EMPTY_DRM_CONFIGURATION = - /** @type {!DrmConfiguration} */ (Object.freeze({ - servers: {}, - })); - -/** - * Creates the configuration of the Shaka player. - */ -class ConfigurationFactory { - /** - * Creates the Shaka player configuration. - * - * @param {!MediaItem} mediaItem The media item for which to create the - * configuration. - * @param {!TrackSelectionParameters} trackSelectionParameters The track - * selection parameters. - * @return {!PlayerConfiguration} The shaka player configuration. - */ - createConfiguration(mediaItem, trackSelectionParameters) { - const configuration = /** @type {!PlayerConfiguration} */ ({}); - this.mapLanguageConfiguration(trackSelectionParameters, configuration); - this.mapDrmConfiguration_(mediaItem, configuration); - return configuration; - } - - /** - * Maps the preferred audio and text language from the track selection - * parameters to the configuration. - * - * @param {!TrackSelectionParameters} trackSelectionParameters The selection - * parameters. - * @param {!PlayerConfiguration} playerConfiguration The player configuration. - */ - mapLanguageConfiguration(trackSelectionParameters, playerConfiguration) { - playerConfiguration.preferredAudioLanguage = - trackSelectionParameters.preferredAudioLanguage || ''; - playerConfiguration.preferredTextLanguage = - trackSelectionParameters.preferredTextLanguage || ''; - } - - /** - * Maps the drm configuration from the media item to the configuration. If no - * drm is specified for the given media item, null is assigned. - * - * @private - * @param {!MediaItem} mediaItem The media item. - * @param {!PlayerConfiguration} playerConfiguration The player configuration. - */ - mapDrmConfiguration_(mediaItem, playerConfiguration) { - if (!mediaItem.drmSchemes) { - playerConfiguration.drm = EMPTY_DRM_CONFIGURATION; - return; - } - const drmConfiguration = /** @type {!DrmConfiguration} */({ - servers: {}, - }); - let hasDrmServer = false; - mediaItem.drmSchemes.forEach((scheme) => { - const drmSystem = DRM_SYSTEMS[scheme.uuid]; - if (drmSystem && scheme.licenseServer && scheme.licenseServer.uri) { - hasDrmServer = true; - drmConfiguration.servers[drmSystem] = scheme.licenseServer.uri; - } - }); - playerConfiguration.drm = - hasDrmServer ? drmConfiguration : EMPTY_DRM_CONFIGURATION; - } -} - -exports = ConfigurationFactory; diff --git a/cast_receiver_app/src/constants.js b/cast_receiver_app/src/constants.js deleted file mode 100644 index e9600429f0..0000000000 --- a/cast_receiver_app/src/constants.js +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright (C) 2019 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. - */ - -goog.module('exoplayer.cast.constants'); - -/** - * The underyling player. - * - * @enum {number} - */ -const PlaybackType = { - VIDEO_ELEMENT: 1, - SHAKA_PLAYER: 2, - UNKNOWN: 999, -}; - -/** - * Supported mime types and their playback mode. - * - * @type {!Object} - */ -const SUPPORTED_MIME_TYPES = Object.freeze({ - 'application/dash+xml': PlaybackType.SHAKA_PLAYER, - 'application/vnd.apple.mpegurl': PlaybackType.SHAKA_PLAYER, - 'application/vnd.ms-sstr+xml': PlaybackType.SHAKA_PLAYER, - 'application/x-mpegURL': PlaybackType.SHAKA_PLAYER, -}); - -/** - * Returns the playback type required for a given mime type, or - * PlaybackType.UNKNOWN if the mime type is not recognized. - * - * @param {string} mimeType The mime type. - * @return {!PlaybackType} The required playback type, or PlaybackType.UNKNOWN - * if the mime type is not recognized. - */ -const getPlaybackType = function(mimeType) { - if (mimeType.startsWith('video/') || mimeType.startsWith('audio/')) { - return PlaybackType.VIDEO_ELEMENT; - } else { - return SUPPORTED_MIME_TYPES[mimeType] || PlaybackType.UNKNOWN; - } -}; - -/** - * Error messages. - * - * @enum {string} - */ -const ErrorMessages = { - SHAKA_LOAD_ERROR: 'Error while loading media with Shaka.', - SHAKA_UNKNOWN_ERROR: 'Shaka error event captured.', - MEDIA_ELEMENT_UNKNOWN_ERROR: 'Media element error event captured.', - UNKNOWN_FATAL_ERROR: 'Fatal playback error. Shaka instance replaced.', - UNKNOWN_ERROR: 'Unknown error', -}; - -/** - * ExoPlayer's repeat modes. - * - * @enum {string} - */ -const RepeatMode = { - OFF: 'OFF', - ONE: 'ONE', - ALL: 'ALL', -}; - -/** - * Error categories. Error categories coming from Shaka are defined in [Shaka - * source - * code](https://shaka-player-demo.appspot.com/docs/api/shaka.util.Error.html). - * - * @enum {number} - */ -const ErrorCategory = { - MEDIA_ELEMENT: 0, - FATAL_SHAKA_ERROR: 1000, -}; - -/** - * An error object to be used if no media error is assigned to the `error` - * field of the media element when an error event is fired - * - * @type {!PlayerError} - */ -const UNKNOWN_ERROR = /** @type {!PlayerError} */ (Object.freeze({ - message: ErrorMessages.UNKNOWN_ERROR, - code: 0, - category: 0, -})); - -/** - * UUID for the Widevine DRM scheme. - * - * @type {string} - */ -const WIDEVINE_UUID = 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'; - -/** - * UUID for the PlayReady DRM scheme. - * - * @type {string} - */ -const PLAYREADY_UUID = '9a04f079-9840-4286-ab92-e65be0885f95'; - -/** @type {!Object} */ -const drmSystems = {}; -drmSystems[WIDEVINE_UUID] = 'com.widevine.alpha'; -drmSystems[PLAYREADY_UUID] = 'com.microsoft.playready'; - -/** - * The uuids of the supported DRM systems. - * - * @type {!Object} - */ -const DRM_SYSTEMS = Object.freeze(drmSystems); - -exports.PlaybackType = PlaybackType; -exports.ErrorMessages = ErrorMessages; -exports.ErrorCategory = ErrorCategory; -exports.RepeatMode = RepeatMode; -exports.getPlaybackType = getPlaybackType; -exports.WIDEVINE_UUID = WIDEVINE_UUID; -exports.PLAYREADY_UUID = PLAYREADY_UUID; -exports.DRM_SYSTEMS = DRM_SYSTEMS; -exports.UNKNOWN_ERROR = UNKNOWN_ERROR; diff --git a/cast_receiver_app/src/playback_info_view.js b/cast_receiver_app/src/playback_info_view.js deleted file mode 100644 index 22e2b8ded5..0000000000 --- a/cast_receiver_app/src/playback_info_view.js +++ /dev/null @@ -1,233 +0,0 @@ -/* - * 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. - */ - -goog.module('exoplayer.cast.PlaybackInfoView'); - -const Player = goog.require('exoplayer.cast.Player'); -const Timeout = goog.require('exoplayer.cast.Timeout'); -const dom = goog.require('goog.dom'); - -/** The default timeout for hiding the UI in milliseconds. */ -const SHOW_TIMEOUT_MS = 5000; -/** The timeout for hiding the UI in audio only mode in milliseconds. */ -const SHOW_TIMEOUT_MS_AUDIO = 0; -/** The timeout for updating the UI while being displayed. */ -const UPDATE_TIMEOUT_MS = 1000; - -/** - * Formats a duration in milliseconds to a string in hh:mm:ss format. - * - * @param {number} durationMs The duration in milliseconds. - * @return {string} The duration formatted as hh:mm:ss. - */ -const formatTimestampMsAsString = function (durationMs) { - const hours = Math.floor(durationMs / 1000 / 60 / 60); - const minutes = Math.floor((durationMs / 1000 / 60) % 60); - const seconds = Math.floor((durationMs / 1000) % 60) % 60; - let timeString = ''; - if (hours > 0) { - timeString += hours + ':'; - } - if (minutes < 10) { - timeString += '0'; - } - timeString += minutes + ":"; - if (seconds < 10) { - timeString += '0'; - } - timeString += seconds; - return timeString; -}; - -/** - * A view to display information about the current media item and playback - * progress. - * - * @constructor - * @param {!Player} player The player of which to display the - * playback info. - * @param {string} viewId The id of the playback info view. - */ -const PlaybackInfoView = function (player, viewId) { - /** @const @private {!Player} */ - this.player_ = player; - /** @const @private {?Element} */ - this.container_ = document.getElementById(viewId); - /** @const @private {?Element} */ - this.elapsedTimeBar_ = document.getElementById('exo_elapsed_time'); - /** @const @private {?Element} */ - this.elapsedTimeLabel_ = document.getElementById('exo_elapsed_time_label'); - /** @const @private {?Element} */ - this.durationLabel_ = document.getElementById('exo_duration_label'); - /** @const @private {!Timeout} */ - this.hideTimeout_ = new Timeout(); - /** @const @private {!Timeout} */ - this.updateTimeout_ = new Timeout(); - /** @private {boolean} */ - this.wasPlaying_ = player.getPlayWhenReady() - && player.getPlaybackState() === Player.PlaybackState.READY; - /** @private {number} */ - this.showTimeoutMs_ = SHOW_TIMEOUT_MS; - /** @private {number} */ - this.showTimeoutMsVideo_ = this.showTimeoutMs_; - - if (this.wasPlaying_) { - this.hideAfterTimeout(); - } else { - this.show(); - } - - player.addPlayerListener((playerState) => { - if (this.container_ === null) { - return; - } - const playbackPosition = playerState.playbackPosition; - const discontinuityReason = - playbackPosition ? playbackPosition.discontinuityReason : null; - if (discontinuityReason) { - const currentMediaItem = player.getCurrentMediaItem(); - this.showTimeoutMs_ = - currentMediaItem && currentMediaItem.mimeType === 'audio/*' ? - SHOW_TIMEOUT_MS_AUDIO : - this.showTimeoutMsVideo_; - } - const playWhenReady = playerState.playWhenReady; - const state = playerState.playbackState; - const isPlaying = playWhenReady && state === Player.PlaybackState.READY; - const userSeekedInBufferedRange = - discontinuityReason === Player.DiscontinuityReason.SEEK && isPlaying; - if (!isPlaying) { - this.show(); - } else if ((!this.wasPlaying_ && isPlaying) || userSeekedInBufferedRange) { - this.hideAfterTimeout(); - } - this.wasPlaying_ = isPlaying; - }); -}; - -/** Shows the player info view. */ -PlaybackInfoView.prototype.show = function () { - if (this.container_ != null) { - this.hideTimeout_.cancel(); - this.updateUi_(); - this.container_.style.display = 'block'; - this.startUpdateTimeout_(); - } -}; - -/** Hides the player info view. */ -PlaybackInfoView.prototype.hideAfterTimeout = function() { - if (this.container_ === null) { - return; - } - this.show(); - this.hideTimeout_.postDelayed(this.showTimeoutMs_).then(() => { - this.container_.style.display = 'none'; - this.updateTimeout_.cancel(); - }); -}; - -/** - * Sets the playback info view timeout. The playback info view is automatically - * hidden after this duration of time has elapsed without show() being called - * again. When playing streams with content type 'audio/*' the view is always - * displayed. - * - * @param {number} showTimeoutMs The duration in milliseconds. A non-positive - * value will cause the view to remain visible indefinitely. - */ -PlaybackInfoView.prototype.setShowTimeoutMs = function(showTimeoutMs) { - this.showTimeoutMs_ = showTimeoutMs; - this.showTimeoutMsVideo_ = showTimeoutMs; -}; - -/** - * Updates all UI components. - * - * @private - */ -PlaybackInfoView.prototype.updateUi_ = function () { - const elapsedTimeMs = this.player_.getCurrentPositionMs(); - const durationMs = this.player_.getDurationMs(); - if (this.elapsedTimeLabel_ !== null) { - this.updateDuration_(this.elapsedTimeLabel_, elapsedTimeMs, false); - } - if (this.durationLabel_ !== null) { - this.updateDuration_(this.durationLabel_, durationMs, true); - } - if (this.elapsedTimeBar_ !== null) { - this.updateProgressBar_(elapsedTimeMs, durationMs); - } -}; - -/** - * Adjust the progress bar indicating the elapsed time relative to the duration. - * - * @private - * @param {number} elapsedTimeMs The elapsed time in milliseconds. - * @param {number} durationMs The duration in milliseconds. - */ -PlaybackInfoView.prototype.updateProgressBar_ = - function(elapsedTimeMs, durationMs) { - if (elapsedTimeMs <= 0 || durationMs <= 0) { - this.elapsedTimeBar_.style.width = 0; - } else { - const widthPercentage = elapsedTimeMs / durationMs * 100; - this.elapsedTimeBar_.style.width = Math.min(100, widthPercentage) + '%'; - } -}; - -/** - * Updates the display value of the duration in the DOM formatted as hh:mm:ss. - * - * @private - * @param {!Element} element The element to update. - * @param {number} durationMs The duration in milliseconds. - * @param {boolean} hideZero If true values of zero and below are not displayed. - */ -PlaybackInfoView.prototype.updateDuration_ = - function (element, durationMs, hideZero) { - while (element.firstChild) { - element.removeChild(element.firstChild); - } - if (durationMs <= 0 && !hideZero) { - element.appendChild(dom.createDom(dom.TagName.SPAN, {}, - formatTimestampMsAsString(0))); - } else if (durationMs > 0) { - element.appendChild(dom.createDom(dom.TagName.SPAN, {}, - formatTimestampMsAsString(durationMs))); - } -}; - -/** - * Starts a repeating timeout that updates the UI every UPDATE_TIMEOUT_MS - * milliseconds. - * - * @private - */ -PlaybackInfoView.prototype.startUpdateTimeout_ = function() { - this.updateTimeout_.cancel(); - if (!this.player_.getPlayWhenReady() || - this.player_.getPlaybackState() !== Player.PlaybackState.READY) { - return; - } - this.updateTimeout_.postDelayed(UPDATE_TIMEOUT_MS).then(() => { - this.updateUi_(); - this.startUpdateTimeout_(); - }); -}; - -exports = PlaybackInfoView; diff --git a/cast_receiver_app/src/player.js b/cast_receiver_app/src/player.js deleted file mode 100644 index d7ffc58f4c..0000000000 --- a/cast_receiver_app/src/player.js +++ /dev/null @@ -1,1522 +0,0 @@ -/* - * 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. - */ - -goog.module('exoplayer.cast.Player'); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const NetworkingEngine = goog.require('shaka.net.NetworkingEngine'); -const ShakaError = goog.require('shaka.util.Error'); -const ShakaPlayer = goog.require('shaka.Player'); -const asserts = goog.require('goog.dom.asserts'); -const googArray = goog.require('goog.array'); -const safedom = goog.require('goog.dom.safe'); -const {ErrorMessages, ErrorCategory, PlaybackType, RepeatMode, getPlaybackType, UNKNOWN_ERROR} = goog.require('exoplayer.cast.constants'); -const {UuidComparator, createUuidComparator, log} = goog.require('exoplayer.cast.util'); -const {assert, fail} = goog.require('goog.asserts'); -const {clamp} = goog.require('goog.math'); - -/** - * Value indicating that no window index is currently set. - */ -const INDEX_UNSET = -1; - -/** - * Estimated time for processing the manifest after download in millisecconds. - * - * See: https://github.com/google/shaka-player/issues/1734 - */ -const MANIFEST_PROCESSING_ESTIMATE_MS = 350; - -/** - * Media element events to listen to. - * - * @enum {string} - */ -const MediaElementEvent = { - ERROR: 'error', - LOADED_DATA: 'loadeddata', - PAUSE: 'pause', - PLAYING: 'playing', - SEEKED: 'seeked', - SEEKING: 'seeking', - WAITING: 'waiting', -}; - -/** - * Shaka events to listen to. - * - * @enum {string} - */ -const ShakaEvent = { - ERROR: 'error', - STREAMING: 'streaming', - TRACKS_CHANGED: 'trackschanged', -}; - -/** - * ExoPlayer's playback states. - * - * @enum {string} - */ -const PlaybackState = { - IDLE: 'IDLE', - BUFFERING: 'BUFFERING', - READY: 'READY', - ENDED: 'ENDED', -}; - -/** - * ExoPlayer's position discontinuity reasons. - * - * @enum {string} - */ -const DiscontinuityReason = { - PERIOD_TRANSITION: 'PERIOD_TRANSITION', - SEEK: 'SEEK', -}; - -/** - * A dummy `MediaIteminfo` to be used while the actual period is not - * yet available. - * - * @const - * @type {!MediaItemInfo} - */ -const DUMMY_MEDIA_ITEM_INFO = Object.freeze({ - isSeekable: false, - isDynamic: true, - positionInFirstPeriodUs: 0, - defaultStartPositionUs: 0, - windowDurationUs: 0, - periods: [{ - id: 1, - durationUs: 0, - }], -}); - -/** - * The Player wraps a Shaka player and maintains a queue of media items. - * - * After construction the player is in `IDLE` state. Calling `#prepare` prepares - * the player with the queue item at the given window index and position. The - * state transitions to `BUFFERING`. When 'playWhenReady' is set to `true` - * playback start when the player becomes 'READY'. - * - * When the player needs to rebuffer the state goes to 'BUFFERING' and becomes - * 'READY' again when playback can be resumed. - * - * The state transitions to `ENDED` when playback reached the end of the last - * item in the queue, when the last item has been removed from the queue if - * `!IDLE`, or when `prepare` is called with an empty queue. Seeking makes the - * player transition away from `ENDED` again. - * - * When `#stop` is called or when a fatal playback error occurs, the player - * transition to `IDLE` state and needs to be prepared again to resume playback. - * - * `playWhenReady`, `repeatMode`, `shuffleModeEnabled` can be manipulated in any - * state, just as media items can be added, moved and removed. - * - * @constructor - * @param {!ShakaPlayer} shakaPlayer The shaka player to wrap. - * @param {!ConfigurationFactory} configurationFactory A factory to create a - * configuration for the Shaka player. - */ -const Player = function(shakaPlayer, configurationFactory) { - /** @private @const {?HTMLMediaElement} */ - this.videoElement_ = shakaPlayer.getMediaElement(); - /** @private @const {!ConfigurationFactory} */ - this.configurationFactory_ = configurationFactory; - /** @private @const {!Array} */ - this.playerListeners_ = []; - /** - * @private - * @const - * {?function(NetworkingEngine.RequestType, (?|null))} - */ - this.manifestResponseFilter_ = (type, response) => { - if (type === NetworkingEngine.RequestType.MANIFEST) { - setTimeout(() => { - this.updateWindowMediaItemInfo_(); - this.invalidate(); - }, MANIFEST_PROCESSING_ESTIMATE_MS); - } - }; - - /** @private {!ShakaPlayer} */ - this.shakaPlayer_ = shakaPlayer; - /** @private {boolean} */ - this.playWhenReady_ = false; - /** @private {boolean} */ - this.shuffleModeEnabled_ = false; - /** @private {!RepeatMode} */ - this.repeatMode_ = RepeatMode.OFF; - /** @private {!TrackSelectionParameters} */ - this.trackSelectionParameters_ = /** @type {!TrackSelectionParameters} */ ({ - preferredAudioLanguage: '', - preferredTextLanguage: '', - disabledTextTrackSelectionFlags: [], - selectUndeterminedTextLanguage: false, - }); - /** @private {number} */ - this.windowIndex_ = INDEX_UNSET; - /** @private {!Array} */ - this.queue_ = []; - /** @private {!Object} */ - this.queueUuidIndexMap_ = {}; - /** @private {!UuidComparator} */ - this.uuidComparator_ = createUuidComparator(this.queueUuidIndexMap_); - - /** @private {!PlaybackState} */ - this.playbackState_ = PlaybackState.IDLE; - /** @private {!MediaItemInfo} */ - this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; - /** @private {number} */ - this.windowPeriodIndex_ = 0; - /** @private {!Object} */ - this.mediaItemInfoMap_ = {}; - /** @private {?PlayerError} */ - this.playbackError_ = null; - /** @private {?DiscontinuityReason} */ - this.discontinuityReason_ = null; - /** @private {!Array} */ - this.shuffleOrder_ = []; - /** @private {number} */ - this.shuffleIndex_ = 0; - /** @private {!PlaybackType} */ - this.playbackType_ = PlaybackType.UNKNOWN; - /** @private {boolean} */ - this.isManifestFilterRegistered_ = false; - /** @private {?string} */ - this.uuidToPrepare_ = null; - - if (!this.shakaPlayer_ || !this.videoElement_) { - throw new Error('an instance of Shaka player with a media element ' + - 'attached to it needs to be passed to the constructor.'); - } - - /** @private @const {function(!Event)} */ - this.playbackStateListener_ = (ev) => { - log(['handle event: ', ev.type]); - let invalid = false; - switch (ev.type) { - case ShakaEvent.STREAMING: { - // Arrives once after prepare when the manifest is available. - const uuid = this.queue_[this.windowIndex_].uuid; - const cachedMediaItemInfo = this.mediaItemInfoMap_[uuid]; - if (!cachedMediaItemInfo || cachedMediaItemInfo.isDynamic) { - this.updateWindowMediaItemInfo_(); - if (this.windowMediaItemInfo_.isDynamic) { - this.registerManifestResponseFilter_(); - } - invalid = true; - } - break; - } - case ShakaEvent.TRACKS_CHANGED: { - // Arrives when tracks have changed either initially or at a period - // boundary. - const periods = this.windowMediaItemInfo_.periods; - const previousPeriodIndex = this.windowPeriodIndex_; - this.evaluateAndSetCurrentPeriod_(periods); - invalid = previousPeriodIndex !== this.windowPeriodIndex_; - if (periods.length && this.windowPeriodIndex_ > 0) { - // Player transitions to next period in multiperiod stream. - this.discontinuityReason_ = this.discontinuityReason_ || - DiscontinuityReason.PERIOD_TRANSITION; - invalid = true; - } - if (this.videoElement_.paused && this.playWhenReady_) { - this.videoElement_.play(); - } - break; - } - case MediaElementEvent.LOADED_DATA: { - // Arrives once when the first frame has been rendered. - if (this.playbackType_ === PlaybackType.VIDEO_ELEMENT) { - const uuid = this.queue_[this.windowIndex_].uuid; - let mediaItemInfo = this.mediaItemInfoMap_[uuid]; - if (!mediaItemInfo || mediaItemInfo.isDynamic) { - mediaItemInfo = this.buildMediaItemInfoFromElement_(); - if (mediaItemInfo !== null) { - this.mediaItemInfoMap_[uuid] = mediaItemInfo; - this.windowMediaItemInfo_ = mediaItemInfo; - } - } - this.evaluateAndSetCurrentPeriod_(mediaItemInfo.periods); - invalid = true; - } - if (this.videoElement_.paused && this.playWhenReady_) { - // Restart after automatic skip to next queue item. - this.videoElement_.play(); - } else if (this.videoElement_.paused) { - // If paused, the PLAYING event will not be fired, hence we transition - // to state READY right here. - this.playbackState_ = PlaybackState.READY; - invalid = true; - } - break; - } - case MediaElementEvent.WAITING: - case MediaElementEvent.SEEKING: { - // Arrives at a user seek or when re-buffering starts. - if (this.playbackState_ !== PlaybackState.BUFFERING) { - this.playbackState_ = PlaybackState.BUFFERING; - invalid = true; - } - break; - } - case MediaElementEvent.PLAYING: - case MediaElementEvent.SEEKED: { - // Arrives at the end of a user seek or after re-buffering. - if (this.playbackState_ !== PlaybackState.READY) { - this.playbackState_ = PlaybackState.READY; - invalid = true; - } - break; - } - case MediaElementEvent.PAUSE: { - // Detects end of media and either skips to next or transitions to ended - // state. - if (this.videoElement_.ended) { - let nextWindowIndex = this.getNextWindowIndex(); - if (nextWindowIndex !== INDEX_UNSET) { - this.seekToWindowInternal_(nextWindowIndex, undefined); - } else { - this.playbackState_ = PlaybackState.ENDED; - invalid = true; - } - } - break; - } - } - if (invalid) { - this.invalidate(); - } - }; - /** @private @const {function(!Event)} */ - this.mediaElementErrorHandler_ = (ev) => { - console.error('Media element error reported in handler'); - this.playbackError_ = !this.videoElement_.error ? UNKNOWN_ERROR : { - message: this.videoElement_.error.message, - code: this.videoElement_.error.code, - category: ErrorCategory.MEDIA_ELEMENT, - }; - this.playbackState_ = PlaybackState.IDLE; - this.uuidToPrepare_ = this.queue_[this.windowIndex_] ? - this.queue_[this.windowIndex_].uuid : - null; - this.invalidate(); - }; - /** @private @const {function(!Event)} */ - this.shakaErrorHandler_ = (ev) => { - const shakaError = /** @type {!ShakaError} */ (ev['detail']); - if (shakaError.severity !== ShakaError.Severity.RECOVERABLE) { - this.fatalShakaError_(shakaError, 'Shaka error reported by error event'); - this.invalidate(); - } else { - console.error('Recoverable Shaka error reported in handler'); - } - }; - - this.shakaPlayer_.addEventListener( - ShakaEvent.STREAMING, this.playbackStateListener_); - this.shakaPlayer_.addEventListener( - ShakaEvent.TRACKS_CHANGED, this.playbackStateListener_); - - this.videoElement_.addEventListener( - MediaElementEvent.LOADED_DATA, this.playbackStateListener_); - this.videoElement_.addEventListener( - MediaElementEvent.WAITING, this.playbackStateListener_); - this.videoElement_.addEventListener( - MediaElementEvent.PLAYING, this.playbackStateListener_); - this.videoElement_.addEventListener( - MediaElementEvent.PAUSE, this.playbackStateListener_); - this.videoElement_.addEventListener( - MediaElementEvent.SEEKING, this.playbackStateListener_); - this.videoElement_.addEventListener( - MediaElementEvent.SEEKED, this.playbackStateListener_); - - // Attach error handlers. - this.shakaPlayer_.addEventListener(ShakaEvent.ERROR, this.shakaErrorHandler_); - this.videoElement_.addEventListener( - MediaElementEvent.ERROR, this.mediaElementErrorHandler_); -}; - -/** - * Adds a listener to the player. - * - * @param {function(!PlayerState)} listener The player listener. - */ -Player.prototype.addPlayerListener = function(listener) { - this.playerListeners_.push(listener); -}; - -/** - * Removes a listener. - * - * @param {function(!Object)} listener The player listener. - */ -Player.prototype.removePlayerListener = function(listener) { - for (let i = 0; i < this.playerListeners_.length; i++) { - if (this.playerListeners_[i] === listener) { - this.playerListeners_.splice(i, 1); - break; - } - } -}; - -/** - * Gets the current PlayerState. - * - * @return {!PlayerState} - */ -Player.prototype.getPlayerState = function() { - return this.buildPlayerState_(); -}; - -/** - * Sends the current playback state to clients. - */ -Player.prototype.invalidate = function() { - const playbackState = this.buildPlayerState_(); - for (let i = 0; i < this.playerListeners_.length; i++) { - this.playerListeners_[i](playbackState); - } -}; - -/** - * Get the audio tracks. - * - * @return {!Array} An array with the track names}. - */ -Player.prototype.getAudioTracks = function() { - return this.windowMediaItemInfo_ !== DUMMY_MEDIA_ITEM_INFO ? - this.shakaPlayer_.getAudioLanguages() : - []; -}; - -/** - * Gets the video tracks. - * - * @return {!Array} An array with the video tracks. - */ -Player.prototype.getVideoTracks = function() { - return this.windowMediaItemInfo_ !== DUMMY_MEDIA_ITEM_INFO ? - this.shakaPlayer_.getVariantTracks() : - []; -}; - -/** - * Gets the playback state. - * - * @return {!PlaybackState} The playback state. - */ -Player.prototype.getPlaybackState = function() { - return this.playbackState_; -}; - -/** - * Gets the playback error if any. - * - * @return {?Object} The playback error. - */ -Player.prototype.getPlaybackError = function() { - return this.playbackError_; -}; - -/** - * Gets the duration in milliseconds or a negative value if unknown. - * - * @return {number} The duration in milliseconds. - */ -Player.prototype.getDurationMs = function() { - return this.windowMediaItemInfo_ ? - this.windowMediaItemInfo_.windowDurationUs / 1000 : -1; -}; - -/** - * Gets the current position in milliseconds or a negative value if not known. - * - * @return {number} The current position in milliseconds. - */ -Player.prototype.getCurrentPositionMs = function() { - if (!this.videoElement_.currentTime) { - return 0; - } - return (this.videoElement_.currentTime * 1000) - - (this.windowMediaItemInfo_.positionInFirstPeriodUs / 1000); -}; - -/** - * Gets the current window index. - * - * @return {number} The current window index. - */ -Player.prototype.getCurrentWindowIndex = function() { - if (this.playbackState_ === PlaybackState.IDLE) { - return this.queueUuidIndexMap_[this.uuidToPrepare_ || ''] || 0; - } - return Math.max(0, this.windowIndex_); -}; - -/** - * Gets the media item of the current window or null if the queue is empty. - * - * @return {?MediaItem} The media item of the current window. - */ -Player.prototype.getCurrentMediaItem = function() { - return this.windowIndex_ >= 0 ? this.queue_[this.windowIndex_] : null; -}; - -/** - * Gets the media item info of the current window index or null if not yet - * available. - * - * @return {?MediaItemInfo} The current media item info or undefined. - */ -Player.prototype.getCurrentMediaItemInfo = function () { - return this.windowMediaItemInfo_; -}; - -/** - * Gets the text tracks. - * - * @return {!TextTrackList} The text tracks. - */ -Player.prototype.getTextTracks = function() { - return this.videoElement_.textTracks; -}; - -/** - * Gets whether the player should play when ready. - * - * @return {boolean} True when it plays when ready. - */ -Player.prototype.getPlayWhenReady = function() { - return this.playWhenReady_; -}; - -/** - * Sets whether to play when ready. - * - * @param {boolean} playWhenReady Whether to play when ready. - * @return {boolean} Whether calling this method causes a change of the player - * state. - */ -Player.prototype.setPlayWhenReady = function(playWhenReady) { - if (this.playWhenReady_ === playWhenReady) { - return false; - } - this.playWhenReady_ = playWhenReady; - this.invalidate(); - if (this.playbackState_ === PlaybackState.IDLE || - this.playbackState_ === PlaybackState.ENDED) { - return true; - } - if (this.playWhenReady_) { - this.videoElement_.play(); - } else { - this.videoElement_.pause(); - } - return true; -}; - -/** - * Gets the repeat mode. - * - * @return {!RepeatMode} The repeat mode. - */ -Player.prototype.getRepeatMode = function() { - return this.repeatMode_; -}; - -/** - * Sets the repeat mode. Must be a value of the enum Player.RepeatMode. - * - * @param {!RepeatMode} mode The repeat mode. - * @return {boolean} Whether calling this method causes a change of the player - * state. - */ -Player.prototype.setRepeatMode = function(mode) { - if (this.repeatMode_ === mode) { - return false; - } - if (mode === Player.RepeatMode.OFF || - mode === Player.RepeatMode.ONE || - mode === Player.RepeatMode.ALL) { - this.repeatMode_ = mode; - } else { - throw new Error('illegal repeat mode: ' + mode); - } - this.invalidate(); - return true; -}; - -/** - * Enables or disables the shuffle mode. - * - * @param {boolean} enabled Whether the shuffle mode is enabled or not. - * @return {boolean} Whether calling this method causes a change of the player - * state. - */ -Player.prototype.setShuffleModeEnabled = function(enabled) { - if (this.shuffleModeEnabled_ === enabled) { - return false; - } - this.shuffleModeEnabled_ = enabled; - this.invalidate(); - return true; -}; - -/** - * Sets the track selection parameters. - * - * @param {!TrackSelectionParameters} trackSelectionParameters The parameters. - * @return {boolean} Whether calling this method causes a change of the player - * state. - */ -Player.prototype.setTrackSelectionParameters = function( - trackSelectionParameters) { - this.trackSelectionParameters_ = trackSelectionParameters; - /** @type {!PlayerConfiguration} */ - const configuration = /** @type {!PlayerConfiguration} */ ({}); - this.configurationFactory_.mapLanguageConfiguration( - trackSelectionParameters, configuration); - /** @type {!PlayerConfiguration} */ - const currentConfiguration = this.shakaPlayer_.getConfiguration(); - /** @type {boolean} */ - let isStateChange = false; - if (currentConfiguration.preferredAudioLanguage !== - configuration.preferredAudioLanguage) { - this.shakaPlayer_.selectAudioLanguage(configuration.preferredAudioLanguage); - isStateChange = true; - } - if (currentConfiguration.preferredTextLanguage !== - configuration.preferredTextLanguage) { - this.shakaPlayer_.selectTextLanguage(configuration.preferredTextLanguage); - isStateChange = true; - } - return isStateChange; -}; - -/** - * Gets the previous window index or a negative number if no item previous to - * the current item is available. - * - * @return {number} The previous window index or a negative number if the - * current item is the first item. - */ -Player.prototype.getPreviousWindowIndex = function() { - if (this.playbackType_ === PlaybackType.UNKNOWN) { - return INDEX_UNSET; - } - switch (this.repeatMode_) { - case RepeatMode.ONE: - return this.windowIndex_; - case RepeatMode.ALL: - if (this.shuffleModeEnabled_) { - const previousIndex = this.shuffleIndex_ > 0 ? - this.shuffleIndex_ - 1 : this.queue_.length - 1; - return this.shuffleOrder_[previousIndex]; - } else { - const previousIndex = this.windowIndex_ > 0 ? - this.windowIndex_ - 1 : this.queue_.length - 1; - return previousIndex; - } - break; - case RepeatMode.OFF: - if (this.shuffleModeEnabled_) { - const previousIndex = this.shuffleIndex_ - 1; - return previousIndex < 0 ? -1 : this.shuffleOrder_[previousIndex]; - } else { - const previousIndex = this.windowIndex_ - 1; - return previousIndex < 0 ? -1 : previousIndex; - } - break; - default: - throw new Error('illegal state of repeat mode: ' + this.repeatMode_); - } -}; - -/** - * Gets the next window index or a negative number if the current item is the - * last item. - * - * @return {number} The next window index or a negative number if the current - * item is the last item. - */ -Player.prototype.getNextWindowIndex = function() { - if (this.playbackType_ === PlaybackType.UNKNOWN) { - return INDEX_UNSET; - } - switch (this.repeatMode_) { - case RepeatMode.ONE: - return this.windowIndex_; - case RepeatMode.ALL: - if (this.shuffleModeEnabled_) { - const nextIndex = (this.shuffleIndex_ + 1) % this.queue_.length; - return this.shuffleOrder_[nextIndex]; - } else { - return (this.windowIndex_ + 1) % this.queue_.length; - } - break; - case RepeatMode.OFF: - if (this.shuffleModeEnabled_) { - const nextIndex = this.shuffleIndex_ + 1; - return nextIndex < this.shuffleOrder_.length ? - this.shuffleOrder_[nextIndex] : -1; - } else { - const nextIndex = this.windowIndex_ + 1; - return nextIndex < this.queue_.length ? nextIndex : -1; - } - break; - default: - throw new Error('illegal state of repeat mode: ' + this.repeatMode_); - } -}; - -/** - * Gets whether the current window is seekable. - * - * @return {boolean} True if seekable. - */ -Player.prototype.isCurrentWindowSeekable = function() { - return !!this.videoElement_.seekable; -}; - -/** - * Seeks to the positionMs of the media item with the given uuid. - * - * @param {string} uuid The uuid of the media item to seek to. - * @param {number|undefined} positionMs The position in milliseconds to seek to. - * @return {boolean} True if a seek operation has been processed, false - * otherwise. - */ -Player.prototype.seekToUuid = function(uuid, positionMs) { - if (this.playbackState_ === PlaybackState.IDLE) { - this.uuidToPrepare_ = uuid; - this.videoElement_.currentTime = - this.getPosition_(positionMs, INDEX_UNSET) / 1000; - this.invalidate(); - return true; - } - const windowIndex = this.queueUuidIndexMap_[uuid]; - if (windowIndex !== undefined) { - positionMs = this.getPosition_(positionMs, windowIndex); - this.discontinuityReason_ = DiscontinuityReason.SEEK; - this.seekToWindowInternal_(windowIndex, positionMs); - return true; - } - return false; -}; - -/** - * Seeks to the positionMs of the given window. - * - * The index must be a valid index of the current queue, else this method does - * nothing. - * - * @param {number} windowIndex The index of the window to seek to. - * @param {number|undefined} positionMs The position to seek to within the - * window. - */ -Player.prototype.seekToWindow = function(windowIndex, positionMs) { - if (windowIndex < 0 || windowIndex >= this.queue_.length) { - return; - } - this.seekToUuid(this.queue_[windowIndex].uuid, positionMs); -}; - -/** - * Gets the number of media items in the queue. - * - * @return {number} The size of the queue. - */ -Player.prototype.getQueueSize = function() { - return this.queue_.length; -}; - -/** - * Adds an array of items at the given index of the queue. - * - * Items are expected to have been validated with `validation#validateMediaItem` - * or `validation#validateMediaItems` before being passed to this method. - * - * @param {number} index The index where to insert the media item. - * @param {!Array} mediaItems The media items. - * @param {!Array|undefined} shuffleOrder The new shuffle order. - * @return {number} The number of added items. - */ -Player.prototype.addQueueItems = function(index, mediaItems, shuffleOrder) { - if (index < 0 || mediaItems.length === 0) { - return 0; - } - let addedItemCount = 0; - index = Math.min(this.queue_.length, index); - mediaItems.forEach((itemToAdd) => { - if (this.queueUuidIndexMap_[itemToAdd.uuid] === undefined) { - this.queue_.splice(index + addedItemCount, 0, itemToAdd); - this.queueUuidIndexMap_[itemToAdd.uuid] = index + addedItemCount; - addedItemCount++; - } - }); - if (addedItemCount === 0) { - return 0; - } - this.buildUuidIndexMap_(index + addedItemCount); - this.setShuffleOrder_(shuffleOrder); - if (this.queue_.length === addedItemCount) { - this.windowIndex_ = 0; - this.updateShuffleIndex_(); - } else if ( - index <= this.windowIndex_ && - this.playbackType_ !== PlaybackType.UNKNOWN) { - this.windowIndex_ += mediaItems.length; - this.updateShuffleIndex_(); - } - this.invalidate(); - return addedItemCount; -}; - -/** - * Removes the queue items with the given uuids. - * - * @param {!Array} uuids The uuids of the queue items to remove. - * @return {number} The number of items removed from the queue. - */ -Player.prototype.removeQueueItems = function(uuids) { - let currentWindowRemoved = false; - let lowestIndexRemoved = this.queue_.length - 1; - const initialQueueSize = this.queue_.length; - // Sort in descending order to start removing from the end. - uuids = uuids.sort(this.uuidComparator_); - uuids.forEach((uuid) => { - const indexToRemove = this.queueUuidIndexMap_[uuid]; - if (indexToRemove === undefined) { - return; - } - // Remove the item from the queue. - this.queue_.splice(indexToRemove, 1); - // Remove the corresponding media item info. - delete this.mediaItemInfoMap_[uuid]; - // Remove the mapping to the window index. - delete this.queueUuidIndexMap_[uuid]; - lowestIndexRemoved = Math.min(lowestIndexRemoved, indexToRemove); - currentWindowRemoved = - currentWindowRemoved || indexToRemove === this.windowIndex_; - // The window index needs to be decreased when the item which has been - // removed was before the current item, when the current item at the last - // position has been removed, or when the queue has been emptied. - if (indexToRemove < this.windowIndex_ || - (indexToRemove === this.windowIndex_ && - indexToRemove === this.queue_.length) || - this.queue_.length === 0) { - this.windowIndex_--; - } - // Adjust the shuffle order. - let shuffleIndexToRemove; - this.shuffleOrder_.forEach((windowIndex, index) => { - if (windowIndex > indexToRemove) { - // Decrease the index in the shuffle order. - this.shuffleOrder_[index]--; - } else if (windowIndex === indexToRemove) { - // Recall index for removal after traversing. - shuffleIndexToRemove = index; - } - }); - // Remove the shuffle order entry of the removed item. - this.shuffleOrder_.splice(shuffleIndexToRemove, 1); - }); - const removedItemsCount = initialQueueSize - this.queue_.length; - if (removedItemsCount === 0) { - return 0; - } - this.updateShuffleIndex_(); - this.buildUuidIndexMap_(lowestIndexRemoved); - if (currentWindowRemoved) { - if (this.queue_.length === 0) { - this.playbackState_ = this.playbackState_ === PlaybackState.IDLE ? - PlaybackState.IDLE : - PlaybackState.ENDED; - this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; - this.windowPeriodIndex_ = 0; - this.videoElement_.currentTime = 0; - this.uuidToPrepare_ = null; - this.unregisterManifestResponseFilter_(); - this.unload_(/** reinitialiseMediaSource= */ true); - } else if (this.windowIndex_ >= 0) { - const windowIndexToPrepare = this.windowIndex_; - this.windowIndex_ = INDEX_UNSET; - this.seekToWindowInternal_(windowIndexToPrepare, undefined); - return removedItemsCount; - } - } - this.invalidate(); - return removedItemsCount; -}; - -/** - * Move the queue item with the given id to the given position. - * - * @param {string} uuid The uuid of the queue item to move. - * @param {number} to The position to move the item to. - * @param {!Array|undefined} shuffleOrder The new shuffle order. - * @return {boolean} Whether the item has been moved. - */ -Player.prototype.moveQueueItem = function(uuid, to, shuffleOrder) { - if (to < 0 || to >= this.queue_.length) { - return false; - } - const windowIndex = this.queueUuidIndexMap_[uuid]; - if (windowIndex === undefined) { - return false; - } - const itemMoved = this.moveInQueue_(windowIndex, to); - if (itemMoved) { - this.setShuffleOrder_(shuffleOrder); - this.invalidate(); - } - return itemMoved; -}; - -/** - * Prepares the player at the current window index and position. - * - * The playback state immediately transitions to `BUFFERING`. If the queue - * is empty the player transitions to `ENDED`. - */ -Player.prototype.prepare = function() { - if (this.queue_.length === 0) { - this.uuidToPrepare_ = null; - this.playbackState_ = PlaybackState.ENDED; - this.invalidate(); - return; - } - if (this.uuidToPrepare_) { - this.windowIndex_ = - this.queueUuidIndexMap_[this.uuidToPrepare_] || INDEX_UNSET; - this.uuidToPrepare_ = null; - } - this.windowIndex_ = clamp(this.windowIndex_, 0, this.queue_.length - 1); - this.prepare_(this.getCurrentPositionMs()); - this.invalidate(); -}; - -/** - * Stops the player. - * - * Calling this method causes the player to transition into `IDLE` state. - * If `reset` is `true` the player is reset to the initial state of right - * after construction. If `reset` is `false`, the media queue is preserved - * and calling `prepare()` results in resuming the player state to what it - * was before calling `#stop(false)`. - * - * @param {boolean} reset Whether the state should be reset. - * @return {!Promise} A promise which resolves after async unload - * tasks have finished. - */ -Player.prototype.stop = function(reset) { - this.playbackState_ = PlaybackState.IDLE; - this.playbackError_ = null; - this.discontinuityReason_ = null; - this.unregisterManifestResponseFilter_(); - this.uuidToPrepare_ = this.uuidToPrepare_ || (this.queue_[this.windowIndex_] ? - this.queue_[this.windowIndex_].uuid : - null); - if (reset) { - this.uuidToPrepare_ = null; - this.queue_ = []; - this.queueUuidIndexMap_ = {}; - this.uuidComparator_ = createUuidComparator(this.queueUuidIndexMap_); - this.windowIndex_ = INDEX_UNSET; - this.mediaItemInfoMap_ = {}; - this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; - this.windowPeriodIndex_ = 0; - this.videoElement_.currentTime = 0; - this.shuffleOrder_ = []; - this.shuffleIndex_ = 0; - } - this.invalidate(); - return this.unload_(/** reinitialiseMediaSource= */ !reset); -}; - -/** - * Resets player and media element. - * - * @private - * @param {boolean} reinitialiseMediaSource Whether the media source should be - * reinitialized. - * @return {!Promise} A promise which resolves after async unload - * tasks have finished. - */ -Player.prototype.unload_ = function(reinitialiseMediaSource) { - const playbackTypeToUnload = this.playbackType_; - this.playbackType_ = PlaybackType.UNKNOWN; - switch (playbackTypeToUnload) { - case PlaybackType.VIDEO_ELEMENT: - this.videoElement_.removeAttribute('src'); - this.videoElement_.load(); - return Promise.resolve(); - case PlaybackType.SHAKA_PLAYER: - return new Promise((resolve, reject) => { - this.shakaPlayer_.unload(reinitialiseMediaSource) - .then(resolve) - .catch(resolve); - }); - default: - return Promise.resolve(); - } -}; - -/** - * Releases the current Shaka instance and create a new one. - * - * This function should only be called if the Shaka instance is out of order due - * to https://github.com/google/shaka-player/issues/1785. It assumes the current - * Shaka instance has fallen into a state in which promises returned by - * `shakaPlayer.load` and `shakaPlayer.unload` do not resolve nor are they - * rejected anymore. - * - * @private - */ -Player.prototype.replaceShaka_ = function() { - // Remove all listeners. - this.shakaPlayer_.removeEventListener( - ShakaEvent.STREAMING, this.playbackStateListener_); - this.shakaPlayer_.removeEventListener( - ShakaEvent.TRACKS_CHANGED, this.playbackStateListener_); - this.shakaPlayer_.removeEventListener( - ShakaEvent.ERROR, this.shakaErrorHandler_); - // Unregister response filter if any. - this.unregisterManifestResponseFilter_(); - // Unload the old instance. - this.shakaPlayer_.unload(false); - // Reset video element. - this.videoElement_.removeAttribute('src'); - this.videoElement_.load(); - // Create a new instance and add listeners. - this.shakaPlayer_ = new ShakaPlayer(this.videoElement_); - this.shakaPlayer_.addEventListener( - ShakaEvent.STREAMING, this.playbackStateListener_); - this.shakaPlayer_.addEventListener( - ShakaEvent.TRACKS_CHANGED, this.playbackStateListener_); - this.shakaPlayer_.addEventListener(ShakaEvent.ERROR, this.shakaErrorHandler_); -}; - -/** - * Moves a queue item within the queue. - * - * @private - * @param {number} from The initial position. - * @param {number} to The position to move the item to. - * @return {boolean} Whether the item has been moved. - */ -Player.prototype.moveInQueue_ = function(from, to) { - if (from < 0 || to < 0 - || from >= this.queue_.length || to >= this.queue_.length - || from === to) { - return false; - } - this.queue_.splice(to, 0, this.queue_.splice(from, 1)[0]); - this.buildUuidIndexMap_(Math.min(from, to)); - if (from === this.windowIndex_) { - this.windowIndex_ = to; - } else if (from > this.windowIndex_ && to <= this.windowIndex_) { - this.windowIndex_++; - } else if (from < this.windowIndex_ && to >= this.windowIndex_) { - this.windowIndex_--; - } - return true; -}; - -/** - * Shuffles the queue. - * - * @private - */ -Player.prototype.shuffle_ = function() { - this.shuffleOrder_ = this.queue_.map((item, index) => index); - googArray.shuffle(this.shuffleOrder_); - this.updateShuffleIndex_(); -}; - -/** - * Sets the new shuffle order. - * - * @private - * @param {!Array|undefined} shuffleOrder The new shuffle order. - */ -Player.prototype.setShuffleOrder_ = function(shuffleOrder) { - if (shuffleOrder && this.queue_.length === shuffleOrder.length) { - this.shuffleOrder_ = shuffleOrder; - this.updateShuffleIndex_(); - } else if (this.shuffleOrder_.length !== this.queue_.length) { - this.shuffle_(); - } -}; - -/** - * Updates the shuffle order to point to the current window index. - * - * @private - */ -Player.prototype.updateShuffleIndex_ = function() { - this.shuffleIndex_ = - this.shuffleOrder_.findIndex((idx) => idx === this.windowIndex_); -}; - -/** - * Builds the `queueUuidIndexMap` using the uuid of a media item as the key and - * the window index as the value of an entry. - * - * @private - * @param {number} startPosition The window index to start updating at. - */ -Player.prototype.buildUuidIndexMap_ = function(startPosition) { - for (let i = startPosition; i < this.queue_.length; i++) { - this.queueUuidIndexMap_[this.queue_[i].uuid] = i; - } -}; - -/** - * Gets the default position of the current window. - * - * @private - * @return {number} The default position of the current window. - */ -Player.prototype.getDefaultPosition_ = function() { - return this.windowMediaItemInfo_.defaultStartPositionUs; -}; - -/** - * Checks whether the given position is buffered. - * - * @private - * @param {number} positionMs The position to check. - * @return {boolean} true if the media data of the current position is buffered. - */ -Player.prototype.isBuffered_ = function(positionMs) { - const ranges = this.videoElement_.buffered; - for (let i = 0; i < ranges.length; i++) { - const start = ranges.start(i) * 1000; - const end = ranges.end(i) * 1000; - if (start <= positionMs && positionMs <= end) { - return true; - } - } - return false; -}; - -/** - * Seeks to the positionMs of the given window. - * - * To signal a user seek, callers are expected to set the discontinuity reason - * to `DiscontinuityReason.SEEK` before calling this method. If not set this - * method may set the `DiscontinuityReason.PERIOD_TRANSITION` in case the - * `windowIndex` changes. - * - * @private - * @param {number} windowIndex The non-negative index of the window to seek to. - * @param {number|undefined} positionMs The position to seek to within the - * window. If undefined it seeks to the default position of the window. - */ -Player.prototype.seekToWindowInternal_ = function(windowIndex, positionMs) { - const windowChanges = this.windowIndex_ !== windowIndex; - // Update window index and position in any case. - this.windowIndex_ = Math.max(0, windowIndex); - this.updateShuffleIndex_(); - const seekPositionMs = this.getPosition_(positionMs, windowIndex); - this.videoElement_.currentTime = seekPositionMs / 1000; - - // IDLE or ENDED with empty queue. - if (this.playbackState_ === PlaybackState.IDLE || this.queue_.length === 0) { - // Do nothing but report the change in window index and position. - this.invalidate(); - return; - } - - // Prepare for a seek to another window or when in ENDED state whilst the - // queue is not empty but prepare has not been called yet. - if (windowChanges || this.playbackType_ === PlaybackType.UNKNOWN) { - // Reset and prepare. - this.unregisterManifestResponseFilter_(); - this.discontinuityReason_ = - this.discontinuityReason_ || DiscontinuityReason.PERIOD_TRANSITION; - this.prepare_(seekPositionMs); - this.invalidate(); - return; - } - - // Sync playWhenReady with video element after ENDED state. - if (this.playbackState_ === PlaybackState.ENDED && this.playWhenReady_) { - this.videoElement_.play(); - return; - } - - // A seek within the current window when READY or BUFFERING. - this.playbackState_ = this.isBuffered_(seekPositionMs) ? - PlaybackState.READY : - PlaybackState.BUFFERING; - this.invalidate(); -}; - -/** - * Prepares the player at the current window index and the given - * `startPositionMs`. - * - * Calling this method resets the media item information, transitions to - * 'BUFFERING', prepares either the plain video element for progressive - * media, or the Shaka player for adaptive media. - * - * Media items are mapped by media type to a `PlaybackType`s in - * `exoplayer.cast.constants.SupportedMediaTypes`. Unsupported mime types will - * cause the player to transition to the `IDLE` state. - * - * Items in the queue are expected to have been validated with - * `validation#validateMediaItem` or `validation#validateMediaItems`. If this is - * not the case this method might throw an Assertion exception. - * - * @private - * @param {number} startPositionMs The position at which to start playback. - * @throws {!AssertionException} In case an unvalidated item can't be mapped to - * a supported playback type. - */ -Player.prototype.prepare_ = function(startPositionMs) { - const mediaItem = this.queue_[this.windowIndex_]; - const windowUuid = this.queue_[this.windowIndex_].uuid; - const mediaItemInfo = this.mediaItemInfoMap_[windowUuid]; - if (mediaItemInfo && !mediaItemInfo.isDynamic) { - // Do reuse if not dynamic. - this.windowMediaItemInfo_ = mediaItemInfo; - } else { - // Use the dummy info until manifest/data available. - this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; - this.mediaItemInfoMap_[windowUuid] = DUMMY_MEDIA_ITEM_INFO; - } - this.windowPeriodIndex_ = 0; - this.playbackType_ = getPlaybackType(mediaItem.mimeType); - this.playbackState_ = PlaybackState.BUFFERING; - const uri = mediaItem.media.uri; - switch (this.playbackType_) { - case PlaybackType.VIDEO_ELEMENT: - this.videoElement_.currentTime = startPositionMs / 1000; - this.shakaPlayer_.unload(false) - .then(() => { - this.setMediaElementSrc(uri); - this.videoElement_.currentTime = startPositionMs / 1000; - }) - .catch((error) => { - // Let's still try. We actually don't need Shaka right now. - this.setMediaElementSrc(uri); - this.videoElement_.currentTime = startPositionMs / 1000; - console.error('Shaka error while unloading', error); - }); - break; - case PlaybackType.SHAKA_PLAYER: - this.shakaPlayer_.configure( - this.configurationFactory_.createConfiguration( - mediaItem, this.trackSelectionParameters_)); - this.shakaPlayer_.load(uri, startPositionMs / 1000).catch((error) => { - const shakaError = /** @type {!ShakaError} */ (error); - if (shakaError.severity !== ShakaError.Severity.RECOVERABLE && - shakaError.code !== ShakaError.Code.LOAD_INTERRUPTED) { - this.fatalShakaError_(shakaError, 'loading failed for uri: ' + uri); - this.invalidate(); - } else { - console.error('Recoverable Shaka error while loading', shakaError); - } - }); - break; - default: - fail('unknown playback type for mime type: ' + mediaItem.mimeType); - } -}; - -/** - * Sets the uri to the `src` attribute of the media element in a safe way. - * - * @param {string} uri The uri to set as the value of the `src` attribute. - */ -Player.prototype.setMediaElementSrc = function(uri) { - safedom.setVideoSrc( - asserts.assertIsHTMLVideoElement(this.videoElement_), uri); -}; - -/** - * Handles a fatal Shaka error by setting the playback error, transitioning to - * state `IDLE` and setting the playback type to `UNKNOWN`. Player needs to be - * reprepared after calling this method. - * - * @private - * @param {!ShakaError} shakaError The error. - * @param {string|undefined} customMessage A custom message. - */ -Player.prototype.fatalShakaError_ = function(shakaError, customMessage) { - this.playbackState_ = PlaybackState.IDLE; - this.playbackType_ = PlaybackType.UNKNOWN; - this.uuidToPrepare_ = this.queue_[this.windowIndex_] ? - this.queue_[this.windowIndex_].uuid : - null; - if (typeof shakaError.severity === 'undefined') { - // Not a Shaka error. We need to assume the worst case. - this.replaceShaka_(); - this.playbackError_ = /** @type {!PlayerError} */ ({ - message: ErrorMessages.UNKNOWN_FATAL_ERROR, - code: -1, - category: ErrorCategory.FATAL_SHAKA_ERROR, - }); - } else { - // A critical ShakaError. Can be recovered from by calling prepare. - this.playbackError_ = /** @type {!PlayerError} */ ({ - message: customMessage || shakaError.message || - ErrorMessages.SHAKA_UNKNOWN_ERROR, - code: shakaError.code, - category: shakaError.category, - }); - } - console.error('caught shaka load error', shakaError); -}; - -/** - * Gets the position to use. If `undefined` or `null` is passed as argument the - * default start position of the media item info of the given windowIndex is - * returned. - * - * @private - * @param {?number|undefined} positionMs The position in milliseconds, - * `undefined` or `null`. - * @param {number} windowIndex The window index for which to evaluate the - * position. - * @return {number} The position to use in milliseconds. - */ -Player.prototype.getPosition_ = function(positionMs, windowIndex) { - if (positionMs !== undefined) { - return Math.max(0, positionMs); - } - const windowUuid = assert(this.queue_[windowIndex]).uuid; - const mediaItemInfo = - this.mediaItemInfoMap_[windowUuid] || DUMMY_MEDIA_ITEM_INFO; - return mediaItemInfo.defaultStartPositionUs; -}; - -/** - * Refreshes the media item info of the current window. - * - * @private - */ -Player.prototype.updateWindowMediaItemInfo_ = function() { - this.windowMediaItemInfo_ = this.buildMediaItemInfo_(); - if (this.windowMediaItemInfo_) { - const mediaItem = this.queue_[this.windowIndex_]; - this.mediaItemInfoMap_[mediaItem.uuid] = this.windowMediaItemInfo_; - this.evaluateAndSetCurrentPeriod_(this.windowMediaItemInfo_.periods); - } -}; - -/** - * Evaluates the current period and stores it in a member variable. - * - * @private - * @param {!Array} periods The periods of the current mediaItem. - */ -Player.prototype.evaluateAndSetCurrentPeriod_ = function(periods) { - const positionUs = this.getCurrentPositionMs() * 1000; - let positionInWindowUs = 0; - periods.some((period, i) => { - positionInWindowUs += period.durationUs; - if (positionUs < positionInWindowUs) { - this.windowPeriodIndex_ = i; - return true; - } - return false; - }); -}; - -/** - * Registers a response filter which is notified when a manifest has been - * downloaded. - * - * @private - */ -Player.prototype.registerManifestResponseFilter_ = function() { - if (this.isManifestFilterRegistered_) { - return; - } - this.shakaPlayer_.getNetworkingEngine().registerResponseFilter( - this.manifestResponseFilter_); - this.isManifestFilterRegistered_ = true; -}; - -/** - * Unregisters the manifest response filter. - * - * @private - */ -Player.prototype.unregisterManifestResponseFilter_ = function() { - if (this.isManifestFilterRegistered_) { - this.shakaPlayer_.getNetworkingEngine().unregisterResponseFilter( - this.manifestResponseFilter_); - this.isManifestFilterRegistered_ = false; - } -}; - -/** - * Builds a MediaItemInfo from the media element. - * - * @private - * @return {!MediaItemInfo} A media item info. - */ -Player.prototype.buildMediaItemInfoFromElement_ = function() { - const durationUs = this.videoElement_.duration * 1000 * 1000; - return /** @type {!MediaItemInfo} */ ({ - isSeekable: !!this.videoElement_.seekable, - isDynamic: false, - positionInFirstPeriodUs: 0, - defaultStartPositionUs: 0, - windowDurationUs: durationUs, - periods: [{ - id: 0, - durationUs: durationUs, - }], - }); -}; - -/** - * Builds a MediaItemInfo from the manifest or null if no manifest is available. - * - * @private - * @return {!MediaItemInfo} - */ -Player.prototype.buildMediaItemInfo_ = function() { - const manifest = this.shakaPlayer_.getManifest(); - if (manifest === null) { - return DUMMY_MEDIA_ITEM_INFO; - } - const timeline = manifest.presentationTimeline; - const isDynamic = timeline.isLive(); - const windowStartUs = isDynamic ? - timeline.getSeekRangeStart() * 1000 * 1000 : - timeline.getSegmentAvailabilityStart() * 1000 * 1000; - const windowDurationUs = isDynamic ? - (timeline.getSeekRangeEnd() - timeline.getSeekRangeStart()) * 1000 * - 1000 : - timeline.getDuration() * 1000 * 1000; - const defaultStartPositionUs = isDynamic ? - timeline.getSeekRangeEnd() * 1000 * 1000 : - timeline.getSegmentAvailabilityStart() * 1000 * 1000; - - const periods = []; - let previousStartTimeUs = 0; - let positionInFirstPeriodUs = 0; - manifest.periods.forEach((period, index) => { - const startTimeUs = period.startTime * 1000 * 1000; - periods.push({ - id: Math.floor(startTimeUs), - }); - if (index > 0) { - // calculate duration of previous period - periods[index - 1].durationUs = startTimeUs - previousStartTimeUs; - if (previousStartTimeUs <= windowStartUs && windowStartUs < startTimeUs) { - positionInFirstPeriodUs = windowStartUs - previousStartTimeUs; - } - } - previousStartTimeUs = startTimeUs; - }); - // calculate duration of last period - if (periods.length) { - const lastPeriodDurationUs = - isDynamic ? Infinity : windowDurationUs - previousStartTimeUs; - periods.slice(-1)[0].durationUs = lastPeriodDurationUs; - if (previousStartTimeUs <= windowStartUs) { - positionInFirstPeriodUs = windowStartUs - previousStartTimeUs; - } - } - return /** @type {!MediaItemInfo} */ ({ - windowDurationUs: Math.floor(windowDurationUs), - defaultStartPositionUs: Math.floor(defaultStartPositionUs), - isSeekable: this.videoElement_ ? !!this.videoElement_.seekable : false, - positionInFirstPeriodUs: Math.floor(positionInFirstPeriodUs), - isDynamic: isDynamic, - periods: periods, - }); -}; - -/** - * Builds the player state message. - * - * @private - * @return {!PlayerState} The player state. - */ -Player.prototype.buildPlayerState_ = function() { - const playerState = { - playbackState: this.getPlaybackState(), - playbackParameters: { - speed: 1, - pitch: 1, - skipSilence: false, - }, - playbackPosition: this.buildPlaybackPosition_(), - playWhenReady: this.getPlayWhenReady(), - windowIndex: this.getCurrentWindowIndex(), - windowCount: this.queue_.length, - audioTracks: this.getAudioTracks() || [], - videoTracks: this.getVideoTracks(), - repeatMode: this.repeatMode_, - shuffleModeEnabled: this.shuffleModeEnabled_, - mediaQueue: this.queue_.slice(), - mediaItemsInfo: this.mediaItemInfoMap_, - shuffleOrder: this.shuffleOrder_, - sequenceNumber: -1, - }; - if (this.playbackError_) { - playerState.error = this.playbackError_; - this.playbackError_ = null; - } - return playerState; -}; - -/** - * Builds the playback position. Returns null if all properties of the playback - * position are empty. - * - * @private - * @return {?PlaybackPosition} The playback position. - */ -Player.prototype.buildPlaybackPosition_ = function() { - if ((this.playbackState_ === PlaybackState.IDLE && !this.uuidToPrepare_) || - this.playbackState_ === PlaybackState.ENDED && this.queue_.length === 0) { - this.discontinuityReason_ = null; - return null; - } - /** @type {!PlaybackPosition} */ - const playbackPosition = { - positionMs: this.getCurrentPositionMs(), - uuid: this.uuidToPrepare_ || this.queue_[this.windowIndex_].uuid, - periodId: this.windowMediaItemInfo_.periods[this.windowPeriodIndex_].id, - discontinuityReason: null, - }; - if (this.discontinuityReason_ !== null) { - playbackPosition.discontinuityReason = this.discontinuityReason_; - this.discontinuityReason_ = null; - } - return playbackPosition; -}; - -exports = Player; -exports.RepeatMode = RepeatMode; -exports.PlaybackState = PlaybackState; -exports.DiscontinuityReason = DiscontinuityReason; -exports.DUMMY_MEDIA_ITEM_INFO = DUMMY_MEDIA_ITEM_INFO; diff --git a/cast_receiver_app/src/timeout.js b/cast_receiver_app/src/timeout.js deleted file mode 100644 index e5df5ec2f4..0000000000 --- a/cast_receiver_app/src/timeout.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -goog.module('exoplayer.cast.Timeout'); - -/** - * A timeout which can be cancelled. - */ -class Timeout { - constructor() { - /** @private {?number} */ - this.timeout_ = null; - } - /** - * Returns a promise which resolves when the duration of time defined by - * delayMs has elapsed and cancel() has not been called earlier. - * - * If the timeout is already set, the former timeout is cancelled and a new - * one is started. - * - * @param {number} delayMs The delay after which to resolve or a non-positive - * value if it should never resolve. - * @return {!Promise} Resolves after the given delayMs or never - * for a non-positive delay. - */ - postDelayed(delayMs) { - this.cancel(); - return new Promise((resolve, reject) => { - if (delayMs <= 0) { - return; - } - this.timeout_ = setTimeout(() => { - if (this.timeout_) { - this.timeout_ = null; - resolve(); - } - }, delayMs); - }); - } - - /** Cancels the timeout. */ - cancel() { - if (this.timeout_) { - clearTimeout(this.timeout_); - this.timeout_ = null; - } - } - - /** @return {boolean} true if the timeout is currently ongoing. */ - isOngoing() { - return this.timeout_ !== null; - } -} - -exports = Timeout; diff --git a/cast_receiver_app/src/util.js b/cast_receiver_app/src/util.js deleted file mode 100644 index 75afd9e5d3..0000000000 --- a/cast_receiver_app/src/util.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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. - */ - -goog.module('exoplayer.cast.util'); - -/** - * Indicates whether the logging is turned on. - */ -const enableLogging = true; - -/** - * Logs to the console if logging enabled. - * - * @param {!Array<*>} statements The log statements to be logged. - */ -const log = function(statements) { - if (enableLogging) { - console.log.apply(console, statements); - } -}; - -/** - * A comparator function for uuids. - * - * @typedef {function(string,string):number} - */ -let UuidComparator; - -/** - * Creates a comparator function which sorts uuids in descending order by the - * corresponding index of the given map. - * - * @param {!Object} uuidIndexMap The map with uuids as the key - * and the window index as the value. - * @return {!UuidComparator} The comparator for sorting. - */ -const createUuidComparator = function(uuidIndexMap) { - return (a, b) => { - const indexA = uuidIndexMap[a] || -1; - const indexB = uuidIndexMap[b] || -1; - return indexB - indexA; - }; -}; - -exports = { - log, - createUuidComparator, - UuidComparator, -}; diff --git a/cast_receiver_app/test/caf_bootstrap.js b/cast_receiver_app/test/caf_bootstrap.js deleted file mode 100644 index 721360e8a7..0000000000 --- a/cast_receiver_app/test/caf_bootstrap.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -/** - * @fileoverview Declares constants which are provided by the CAF externs and - * are not included in uncompiled unit tests. - */ -cast = { - framework: { - system: { - EventType: { - SENDER_CONNECTED: 'sender_connected', - SENDER_DISCONNECTED: 'sender_disconnected', - }, - DisconnectReason: { - REQUESTED_BY_SENDER: 'requested_by_sender', - }, - }, - }, -}; diff --git a/cast_receiver_app/test/configuration_factory_test.js b/cast_receiver_app/test/configuration_factory_test.js deleted file mode 100644 index af9254c59e..0000000000 --- a/cast_receiver_app/test/configuration_factory_test.js +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -goog.module('exoplayer.cast.test.configurationfactory'); -goog.setTestOnly(); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const testSuite = goog.require('goog.testing.testSuite'); -const util = goog.require('exoplayer.cast.test.util'); - -let configurationFactory; - -testSuite({ - setUp() { - configurationFactory = new ConfigurationFactory(); - }, - - /** Tests creating the most basic configuration. */ - testCreateBasicConfiguration() { - /** @type {!TrackSelectionParameters} */ - const selectionParameters = /** @type {!TrackSelectionParameters} */ ({ - preferredAudioLanguage: 'en', - preferredTextLanguage: 'it', - }); - const configuration = configurationFactory.createConfiguration( - util.queue.slice(0, 1), selectionParameters); - assertEquals('en', configuration.preferredAudioLanguage); - assertEquals('it', configuration.preferredTextLanguage); - // Assert empty drm configuration as default. - assertArrayEquals(['servers'], Object.keys(configuration.drm)); - assertArrayEquals([], Object.keys(configuration.drm.servers)); - }, - - /** Tests defaults for undefined audio and text languages. */ - testCreateBasicConfiguration_languagesUndefined() { - const configuration = configurationFactory.createConfiguration( - util.queue.slice(0, 1), /** @type {!TrackSelectionParameters} */ ({})); - assertEquals('', configuration.preferredAudioLanguage); - assertEquals('', configuration.preferredTextLanguage); - }, - - /** Tests creating a drm configuration */ - testCreateDrmConfiguration() { - /** @type {!MediaItem} */ - const mediaItem = util.queue[1]; - mediaItem.drmSchemes = [ - { - uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', - licenseServer: { - uri: 'drm-uri0', - }, - }, - { - uuid: '9a04f079-9840-4286-ab92-e65be0885f95', - licenseServer: { - uri: 'drm-uri1', - }, - }, - { - uuid: 'unsupported-drm-uuid', - licenseServer: { - uri: 'drm-uri2', - }, - }, - ]; - const configuration = - configurationFactory.createConfiguration(mediaItem, {}); - assertEquals('drm-uri0', configuration.drm.servers['com.widevine.alpha']); - assertEquals( - 'drm-uri1', configuration.drm.servers['com.microsoft.playready']); - assertEquals(2, Object.entries(configuration.drm.servers).length); - } -}); diff --git a/cast_receiver_app/test/externs.js b/cast_receiver_app/test/externs.js deleted file mode 100644 index a90a367691..0000000000 --- a/cast_receiver_app/test/externs.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -/** - * Externs for unit tests to avoid renaming of properties. - * - * These externs are only required when building with bazel because the - * closure_js_test compiles tests as well. - * - * @externs - */ - -/** @record */ -function ValidationObject() {} - -/** @type {*} */ -ValidationObject.prototype.field; - -/** @record */ -function Uuids() {} - -/** @type {!Array} */ -Uuids.prototype.uuids; diff --git a/cast_receiver_app/test/message_dispatcher_test.js b/cast_receiver_app/test/message_dispatcher_test.js deleted file mode 100644 index 3e7daaf573..0000000000 --- a/cast_receiver_app/test/message_dispatcher_test.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (C) 2019 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. - * - * @fileoverview Unit tests for the message dispatcher. - */ - -goog.module('exoplayer.cast.test.messagedispatcher'); -goog.setTestOnly(); - -const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher'); -const mocks = goog.require('exoplayer.cast.test.mocks'); -const testSuite = goog.require('goog.testing.testSuite'); - -let contextMock; -let messageDispatcher; - -testSuite({ - setUp() { - mocks.setUp(); - contextMock = mocks.createCastReceiverContextFake(); - messageDispatcher = new MessageDispatcher( - 'urn:x-cast:com.google.exoplayer.cast', contextMock); - }, - - /** Test marshalling Infinity */ - testStringifyInfinity() { - const senderId = 'sender0'; - const name = 'Federico Vespucci'; - messageDispatcher.send(senderId, {name: name, duration: Infinity}); - - const msg = mocks.state().outputMessages[senderId][0]; - assertUndefined(msg.duration); - assertFalse(msg.hasOwnProperty('duration')); - assertEquals(name, msg.name); - assertTrue(msg.hasOwnProperty('name')); - } -}); diff --git a/cast_receiver_app/test/mocks.js b/cast_receiver_app/test/mocks.js deleted file mode 100644 index 244ac72829..0000000000 --- a/cast_receiver_app/test/mocks.js +++ /dev/null @@ -1,277 +0,0 @@ -/** - * 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. - * - * @fileoverview Mocks for testing cast components. - */ - -goog.module('exoplayer.cast.test.mocks'); -goog.setTestOnly(); - -const NetworkingEngine = goog.require('shaka.net.NetworkingEngine'); - -let mockState; -let manifest; - -/** - * Initializes the state of the mocks. Needs to be called in the setUp method of - * the unit test. - */ -const setUp = function() { - mockState = { - outputMessages: {}, - listeners: {}, - loadedUri: null, - preferredTextLanguage: '', - preferredAudioLanguage: '', - configuration: null, - responseFilter: null, - isSilent: false, - customMessageListener: undefined, - mediaElementState: { - removedAttributes: [], - }, - manifestState: { - isLive: false, - windowDuration: 20, - startTime: 0, - delay: 10, - }, - getManifest: () => manifest, - setManifest: (m) => { - manifest = m; - }, - shakaError: { - severity: /** CRITICAL */ 2, - code: /** not 7000 (LOAD_INTERUPTED) */ 3, - category: /** any */ 1, - }, - simulateLoad: simulateLoadSuccess, - /** @type {function(boolean)} */ - setShakaThrowsOnLoad: (doThrow) => { - mockState.simulateLoad = doThrow ? throwShakaError : simulateLoadSuccess; - }, - simulateUnload: simulateUnloadSuccess, - /** @type {function(boolean)} */ - setShakaThrowsOnUnload: (doThrow) => { - mockState.simulateUnload = - doThrow ? throwShakaError : simulateUnloadSuccess; - }, - onSenderConnected: undefined, - onSenderDisconnected: undefined, - }; - manifest = { - periods: [{startTime: mockState.manifestState.startTime}], - presentationTimeline: { - getDuration: () => mockState.manifestState.windowDuration, - isLive: () => mockState.manifestState.isLive, - getSegmentAvailabilityStart: () => 0, - getSegmentAvailabilityEnd: () => mockState.manifestState.windowDuration, - getSeekRangeStart: () => 0, - getSeekRangeEnd: () => mockState.manifestState.windowDuration - - mockState.manifestState.delay, - }, - }; -}; - -/** - * Simulates a successful `shakaPlayer.load` call. - * - * @param {string} uri The uri to load. - */ -const simulateLoadSuccess = (uri) => { - mockState.loadedUri = uri; - notifyListeners('streaming'); -}; - -/** Simulates a successful `shakaPlayer.unload` call. */ -const simulateUnloadSuccess = () => { - mockState.loadedUri = undefined; - notifyListeners('unloading'); -}; - -/** @throws {!ShakaError} Thrown in any case. */ -const throwShakaError = () => { - throw mockState.shakaError; -}; - - -/** - * Adds a fake event listener. - * - * @param {string} type The type of the listener. - * @param {function(!Object)} listener The callback listener. - */ -const addEventListener = function(type, listener) { - mockState.listeners[type] = mockState.listeners[type] || []; - mockState.listeners[type].push(listener); -}; - -/** - * Notifies the fake listeners of the given type. - * - * @param {string} type The type of the listener to notify. - */ -const notifyListeners = function(type) { - if (mockState.isSilent || !mockState.listeners[type]) { - return; - } - for (let i = 0; i < mockState.listeners[type].length; i++) { - mockState.listeners[type][i]({ - type: type - }); - } -}; - -/** - * Creates an observable for which listeners can be added. - * - * @return {!Object} An observable object. - */ -const createObservable = () => { - return { - addEventListener: (type, listener) => { - addEventListener(type, listener); - }, - }; -}; - -/** - * Creates a fake for the shaka player. - * - * @return {!shaka.Player} A shaka player mock object. - */ -const createShakaFake = () => { - const shakaFake = /** @type {!shaka.Player} */(createObservable()); - const mediaElement = createMediaElementFake(); - /** - * @return {!HTMLMediaElement} A media element. - */ - shakaFake.getMediaElement = () => mediaElement; - shakaFake.getAudioLanguages = () => []; - shakaFake.getVariantTracks = () => []; - shakaFake.configure = (configuration) => { - mockState.configuration = configuration; - return true; - }; - shakaFake.selectTextLanguage = (language) => { - mockState.preferredTextLanguage = language; - }; - shakaFake.selectAudioLanguage = (language) => { - mockState.preferredAudioLanguage = language; - }; - shakaFake.getManifest = () => manifest; - shakaFake.unload = async () => mockState.simulateUnload(); - shakaFake.load = async (uri) => mockState.simulateLoad(uri); - shakaFake.getNetworkingEngine = () => { - return /** @type {!NetworkingEngine} */ ({ - registerResponseFilter: (responseFilter) => { - mockState.responseFilter = responseFilter; - }, - unregisterResponseFilter: (responseFilter) => { - if (mockState.responseFilter !== responseFilter) { - throw new Error('unregistering invalid response filter'); - } else { - mockState.responseFilter = null; - } - }, - }); - }; - return shakaFake; -}; - -/** - * Creates a fake for a media element. - * - * @return {!HTMLMediaElement} A media element fake. - */ -const createMediaElementFake = () => { - const mediaElementFake = /** @type {!HTMLMediaElement} */(createObservable()); - mediaElementFake.load = () => { - // Do nothing. - }; - mediaElementFake.play = () => { - mediaElementFake.paused = false; - notifyListeners('playing'); - return Promise.resolve(); - }; - mediaElementFake.pause = () => { - mediaElementFake.paused = true; - notifyListeners('pause'); - }; - mediaElementFake.seekable = /** @type {!TimeRanges} */({ - length: 1, - start: (index) => mockState.manifestState.startTime, - end: (index) => mockState.manifestState.windowDuration, - }); - mediaElementFake.removeAttribute = (name) => { - mockState.mediaElementState.removedAttributes.push(name); - if (name === 'src') { - mockState.loadedUri = null; - } - }; - mediaElementFake.hasAttribute = (name) => { - return name === 'src' && !!mockState.loadedUri; - }; - mediaElementFake.buffered = /** @type {!TimeRanges} */ ({ - length: 0, - start: (index) => null, - end: (index) => null, - }); - mediaElementFake.paused = true; - return mediaElementFake; -}; - -/** - * Creates a cast receiver manager fake. - * - * @return {!Object} A cast receiver manager fake. - */ -const createCastReceiverContextFake = () => { - return { - addCustomMessageListener: (namespace, listener) => { - mockState.customMessageListener = listener; - }, - sendCustomMessage: (namespace, senderId, message) => { - mockState.outputMessages[senderId] = - mockState.outputMessages[senderId] || []; - mockState.outputMessages[senderId].push(message); - }, - addEventListener: (eventName, listener) => { - switch (eventName) { - case 'sender_connected': - mockState.onSenderConnected = listener; - break; - case 'sender_disconnected': - mockState.onSenderDisconnected = listener; - break; - } - }, - getSenders: () => [{id: 'sender0'}], - start: () => {}, - }; -}; - -/** - * Returns the state of the mocks. - * - * @return {?Object} - */ -const state = () => mockState; - -exports.createCastReceiverContextFake = createCastReceiverContextFake; -exports.createShakaFake = createShakaFake; -exports.notifyListeners = notifyListeners; -exports.setUp = setUp; -exports.state = state; diff --git a/cast_receiver_app/test/playback_info_view_test.js b/cast_receiver_app/test/playback_info_view_test.js deleted file mode 100644 index 87cefe1884..0000000000 --- a/cast_receiver_app/test/playback_info_view_test.js +++ /dev/null @@ -1,242 +0,0 @@ -/** - * Copyright (C) 2019 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. - * - * @fileoverview Unit tests for the playback info view. - */ - -goog.module('exoplayer.cast.test.PlaybackInfoView'); -goog.setTestOnly(); - -const PlaybackInfoView = goog.require('exoplayer.cast.PlaybackInfoView'); -const Player = goog.require('exoplayer.cast.Player'); -const testSuite = goog.require('goog.testing.testSuite'); - -/** The state of the player mock */ -let mockState; - -/** - * Initializes the state of the mock. Needs to be called in the setUp method of - * the unit test. - */ -const setUpMockState = function() { - mockState = { - playWhenReady: false, - currentPositionMs: 1000, - durationMs: 10 * 1000, - playbackState: 'READY', - discontinuityReason: undefined, - listeners: [], - currentMediaItem: { - mimeType: 'video/*', - }, - }; -}; - -/** Notifies registered listeners with the current player state. */ -const notifyListeners = function() { - if (!mockState) { - console.warn( - 'mock state not initialized. Did you call setUp ' + - 'when setting up the test case?'); - } - mockState.listeners.forEach((listener) => { - listener({ - playWhenReady: mockState.playWhenReady, - playbackState: mockState.playbackState, - playbackPosition: { - currentPositionMs: mockState.currentPositionMs, - discontinuityReason: mockState.discontinuityReason, - }, - }); - }); -}; - -/** - * Creates a sufficient mock of the Player. - * - * @return {!Player} - */ -const createPlayerMock = function() { - return /** @type {!Player} */ ({ - addPlayerListener: (listener) => { - mockState.listeners.push(listener); - }, - getPlayWhenReady: () => mockState.playWhenReady, - getPlaybackState: () => mockState.playbackState, - getCurrentPositionMs: () => mockState.currentPositionMs, - getDurationMs: () => mockState.durationMs, - getCurrentMediaItem: () => mockState.currentMediaItem, - }); -}; - -/** Inserts the DOM structure the playback info view needs. */ -const insertComponentDom = function() { - const container = appendChild(document.body, 'div', 'container-id'); - appendChild(container, 'div', 'exo_elapsed_time'); - appendChild(container, 'div', 'exo_elapsed_time_label'); - appendChild(container, 'div', 'exo_duration_label'); -}; - -/** - * Creates and appends a child to the parent element. - * - * @param {!Element} parent The parent element. - * @param {string} tagName The tag name of the child element. - * @param {string} id The id of the child element. - * @return {!Element} The appended child element. - */ -const appendChild = function(parent, tagName, id) { - const child = document.createElement(tagName); - child.id = id; - parent.appendChild(child); - return child; -}; - -/** Removes the inserted elements from the DOM again. */ -const removeComponentDom = function() { - const container = document.getElementById('container-id'); - if (container) { - container.parentNode.removeChild(container); - } -}; - -let playbackInfoView; - -testSuite({ - setUp() { - insertComponentDom(); - setUpMockState(); - playbackInfoView = new PlaybackInfoView( - createPlayerMock(), /** containerId= */ 'container-id'); - playbackInfoView.setShowTimeoutMs(1); - }, - - tearDown() { - removeComponentDom(); - }, - - /** Tests setting the show timeout. */ - testSetShowTimeout() { - assertEquals(1, playbackInfoView.showTimeoutMs_); - playbackInfoView.setShowTimeoutMs(10); - assertEquals(10, playbackInfoView.showTimeoutMs_); - }, - - /** Tests rendering the duration to the DOM. */ - testRenderDuration() { - const el = document.getElementById('exo_duration_label'); - assertEquals('00:10', el.firstChild.firstChild.nodeValue); - mockState.durationMs = 35 * 1000; - notifyListeners(); - assertEquals('00:35', el.firstChild.firstChild.nodeValue); - - mockState.durationMs = - (12 * 60 * 60 * 1000) + (20 * 60 * 1000) + (13 * 1000); - notifyListeners(); - assertEquals('12:20:13', el.firstChild.firstChild.nodeValue); - - mockState.durationMs = -1000; - notifyListeners(); - assertNull(el.nodeValue); - }, - - /** Tests rendering the playback position to the DOM. */ - testRenderPlaybackPosition() { - const el = document.getElementById('exo_elapsed_time_label'); - assertEquals('00:01', el.firstChild.firstChild.nodeValue); - mockState.currentPositionMs = 2000; - notifyListeners(); - assertEquals('00:02', el.firstChild.firstChild.nodeValue); - - mockState.currentPositionMs = - (12 * 60 * 60 * 1000) + (20 * 60 * 1000) + (13 * 1000); - notifyListeners(); - assertEquals('12:20:13', el.firstChild.firstChild.nodeValue); - - mockState.currentPositionMs = -1000; - notifyListeners(); - assertNull(el.nodeValue); - - mockState.currentPositionMs = 0; - notifyListeners(); - assertEquals('00:00', el.firstChild.firstChild.nodeValue); - }, - - /** Tests rendering the timebar width reflects position and duration. */ - testRenderTimebar() { - const el = document.getElementById('exo_elapsed_time'); - assertEquals('10%', el.style.width); - - mockState.currentPositionMs = 0; - notifyListeners(); - assertEquals('0px', el.style.width); - - mockState.currentPositionMs = 5 * 1000; - notifyListeners(); - assertEquals('50%', el.style.width); - - mockState.currentPositionMs = mockState.durationMs * 2; - notifyListeners(); - assertEquals('100%', el.style.width); - - mockState.currentPositionMs = -1; - notifyListeners(); - assertEquals('0px', el.style.width); - }, - - /** Tests whether the update timeout is set and removed. */ - testUpdateTimeout_setAndRemoved() { - assertFalse(playbackInfoView.updateTimeout_.isOngoing()); - - mockState.playWhenReady = true; - notifyListeners(); - assertTrue(playbackInfoView.updateTimeout_.isOngoing()); - - mockState.playWhenReady = false; - notifyListeners(); - assertFalse(playbackInfoView.updateTimeout_.isOngoing()); - }, - - /** Tests whether the show timeout is set when playback starts. */ - testHideTimeout_setAndRemoved() { - assertFalse(playbackInfoView.hideTimeout_.isOngoing()); - - mockState.playWhenReady = true; - notifyListeners(); - assertNotUndefined(playbackInfoView.hideTimeout_); - assertTrue(playbackInfoView.hideTimeout_.isOngoing()); - - mockState.playWhenReady = false; - notifyListeners(); - assertFalse(playbackInfoView.hideTimeout_.isOngoing()); - }, - - /** Test whether the view switches to always on for audio media. */ - testAlwaysOnForAudio() { - playbackInfoView.setShowTimeoutMs(50); - assertEquals(50, playbackInfoView.showTimeoutMs_); - // The player transitions from video to audio stream. - mockState.discontinuityReason = 'PERIOD_TRANSITION'; - mockState.currentMediaItem.mimeType = 'audio/*'; - notifyListeners(); - assertEquals(0, playbackInfoView.showTimeoutMs_); - - mockState.discontinuityReason = 'PERIOD_TRANSITION'; - mockState.currentMediaItem.mimeType = 'video/*'; - notifyListeners(); - assertEquals(50, playbackInfoView.showTimeoutMs_); - }, - -}); diff --git a/cast_receiver_app/test/player_test.js b/cast_receiver_app/test/player_test.js deleted file mode 100644 index 96dfbf8614..0000000000 --- a/cast_receiver_app/test/player_test.js +++ /dev/null @@ -1,470 +0,0 @@ -/** - * 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. - * - * @fileoverview Unit tests for playback methods. - */ - -goog.module('exoplayer.cast.test'); -goog.setTestOnly(); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const Player = goog.require('exoplayer.cast.Player'); -const mocks = goog.require('exoplayer.cast.test.mocks'); -const testSuite = goog.require('goog.testing.testSuite'); -const util = goog.require('exoplayer.cast.test.util'); - -let player; -let shakaFake; - -testSuite({ - setUp() { - mocks.setUp(); - shakaFake = mocks.createShakaFake(); - player = new Player(shakaFake, new ConfigurationFactory()); - }, - - /** Tests the player initialisation */ - testPlayerInitialisation() { - mocks.state().isSilent = true; - const states = []; - let stateCounter = 0; - let currentState; - player.addPlayerListener((playerState) => { - states.push(playerState); - }); - - // Dump the initial state manually. - player.invalidate(); - stateCounter++; - assertEquals(stateCounter, states.length); - currentState = states[stateCounter - 1]; - assertEquals(0, currentState.mediaQueue.length); - assertEquals(0, currentState.windowIndex); - assertNull(currentState.playbackPosition); - - // Seek with uuid to prepare with later - const uuid = 'uuid1'; - player.seekToUuid(uuid, 30 * 1000); - stateCounter++; - assertEquals(stateCounter, states.length); - currentState = states[stateCounter - 1]; - assertEquals(30 * 1000, player.getCurrentPositionMs()); - assertEquals(0, player.getCurrentWindowIndex()); - assertEquals(-1, player.windowIndex_); - assertEquals(1, currentState.playbackPosition.periodId); - assertEquals(uuid, currentState.playbackPosition.uuid); - assertEquals(uuid, player.uuidToPrepare_); - - // Add a DASH media item. - player.addQueueItems(0, util.queue.slice(0, 2)); - stateCounter++; - assertEquals(stateCounter, states.length); - currentState = states[stateCounter - 1]; - assertEquals('IDLE', currentState.playbackState); - assertNotNull(currentState.playbackPosition); - util.assertUuidIndexMap(player.queueUuidIndexMap_, currentState.mediaQueue); - - // Prepare. - player.prepare(); - stateCounter++; - assertEquals(stateCounter, states.length); - currentState = states[stateCounter - 1]; - assertEquals(2, currentState.mediaQueue.length); - assertEquals('BUFFERING', currentState.playbackState); - assertEquals( - Player.DUMMY_MEDIA_ITEM_INFO, currentState.mediaItemsInfo[uuid]); - assertNull(player.uuidToPrepare_); - - // The video element starts waiting. - mocks.state().isSilent = false; - mocks.notifyListeners('waiting'); - // Nothing happens, masked buffering state after preparing. - assertEquals(stateCounter, states.length); - - // The manifest arrives. - mocks.notifyListeners('streaming'); - stateCounter++; - assertEquals(stateCounter, states.length); - currentState = states[stateCounter - 1]; - assertEquals(2, currentState.mediaQueue.length); - assertEquals('BUFFERING', currentState.playbackState); - assertEquals(uuid, currentState.playbackPosition.uuid); - assertEquals(0, currentState.playbackPosition.periodId); - assertEquals(30 * 1000, currentState.playbackPosition.positionMs); - // The dummy media item info has been replaced by the real one. - assertEquals(20000000, currentState.mediaItemsInfo[uuid].windowDurationUs); - assertEquals(0, currentState.mediaItemsInfo[uuid].defaultStartPositionUs); - assertEquals(0, currentState.mediaItemsInfo[uuid].positionInFirstPeriodUs); - assertTrue(currentState.mediaItemsInfo[uuid].isSeekable); - assertFalse(currentState.mediaItemsInfo[uuid].isDynamic); - - // Tracks have initially changed. - mocks.notifyListeners('trackschanged'); - // Nothing happens because the media item info remains the same. - assertEquals(stateCounter, states.length); - - // The video element reports the first frame rendered. - mocks.notifyListeners('loadeddata'); - stateCounter++; - assertEquals(stateCounter, states.length); - currentState = states[stateCounter - 1]; - assertEquals(2, currentState.mediaQueue.length); - assertEquals('READY', currentState.playbackState); - assertEquals(uuid, currentState.playbackPosition.uuid); - assertEquals(0, currentState.playbackPosition.periodId); - assertEquals(30 * 1000, currentState.playbackPosition.positionMs); - - // Playback starts. - mocks.notifyListeners('playing'); - // Nothing happens; we are ready already. - assertEquals(stateCounter, states.length); - - // Add another queue item. - player.addQueueItems(1, util.queue.slice(3, 4)); - stateCounter++; - assertEquals(stateCounter, states.length); - mocks.state().isSilent = true; - // Seek to the next queue item. - player.seekToWindow(1, 0); - stateCounter++; - assertEquals(stateCounter, states.length); - currentState = states[stateCounter - 1]; - const uuid1 = currentState.mediaQueue[1].uuid; - assertEquals( - Player.DUMMY_MEDIA_ITEM_INFO, currentState.mediaItemsInfo[uuid1]); - util.assertUuidIndexMap(player.queueUuidIndexMap_, currentState.mediaQueue); - - // The video element starts waiting. - mocks.state().isSilent = false; - mocks.notifyListeners('waiting'); - // Nothing happens, masked buffering state after preparing. - assertEquals(stateCounter, states.length); - - // The manifest arrives. - mocks.notifyListeners('streaming'); - stateCounter++; - assertEquals(stateCounter, states.length); - currentState = states[stateCounter - 1]; - // The dummy media item info has been replaced by the real one. - assertEquals(20000000, currentState.mediaItemsInfo[uuid].windowDurationUs); - assertEquals(0, currentState.mediaItemsInfo[uuid].defaultStartPositionUs); - assertEquals(0, currentState.mediaItemsInfo[uuid].positionInFirstPeriodUs); - assertTrue(currentState.mediaItemsInfo[uuid].isSeekable); - assertFalse(currentState.mediaItemsInfo[uuid].isDynamic); - }, - - /** Tests next and previous window when not yet prepared. */ - testNextPreviousWindow_notPrepared() { - assertEquals(-1, player.getNextWindowIndex()); - assertEquals(-1, player.getPreviousWindowIndex()); - player.addQueueItems(0, util.queue.slice(0, 2)); - assertEquals(-1, player.getNextWindowIndex()); - assertEquals(-1, player.getPreviousWindowIndex()); - }, - - /** Tests setting play when ready. */ - testPlayWhenReady() { - player.addQueueItems(0, util.queue.slice(0, 3)); - let playWhenReady = false; - player.addPlayerListener((state) => { - playWhenReady = state.playWhenReady; - }); - - assertEquals(false, player.getPlayWhenReady()); - assertEquals(false, playWhenReady); - - player.setPlayWhenReady(true); - assertEquals(true, player.getPlayWhenReady()); - assertEquals(true, playWhenReady); - - player.setPlayWhenReady(false); - assertEquals(false, player.getPlayWhenReady()); - assertEquals(false, playWhenReady); - }, - - /** Tests seeking to another position in the actual window. */ - async testSeek_inWindow() { - player.addQueueItems(0, util.queue.slice(0, 3)); - await player.seekToWindow(0, 1000); - - assertEquals(1, shakaFake.getMediaElement().currentTime); - assertEquals(1000, player.getCurrentPositionMs()); - assertEquals(0, player.getCurrentWindowIndex()); - }, - - /** Tests seeking to another window. */ - async testSeek_nextWindow() { - player.addQueueItems(0, util.queue.slice(0, 3)); - await player.prepare(); - assertEquals(util.queue[0].media.uri, shakaFake.getMediaElement().src); - assertEquals(-1, player.getPreviousWindowIndex()); - assertEquals(1, player.getNextWindowIndex()); - - player.seekToWindow(1, 2000); - assertEquals(0, player.getPreviousWindowIndex()); - assertEquals(2, player.getNextWindowIndex()); - assertEquals(2000, player.getCurrentPositionMs()); - assertEquals(1, player.getCurrentWindowIndex()); - assertEquals(util.queue[1].media.uri, mocks.state().loadedUri); - }, - - /** Tests the repeat mode 'none' */ - testRepeatMode_none() { - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - assertEquals(Player.RepeatMode.OFF, player.getRepeatMode()); - assertEquals(-1, player.getPreviousWindowIndex()); - assertEquals(1, player.getNextWindowIndex()); - - player.seekToWindow(2, 0); - assertEquals(1, player.getPreviousWindowIndex()); - assertEquals(-1, player.getNextWindowIndex()); - }, - - /** Tests the repeat mode 'all'. */ - testRepeatMode_all() { - let repeatMode; - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.addPlayerListener((state) => { - repeatMode = state.repeatMode; - }); - player.setRepeatMode(Player.RepeatMode.ALL); - assertEquals(Player.RepeatMode.ALL, repeatMode); - - player.seekToWindow(0,0); - assertEquals(2, player.getPreviousWindowIndex()); - assertEquals(1, player.getNextWindowIndex()); - - player.seekToWindow(2, 0); - assertEquals(1, player.getPreviousWindowIndex()); - assertEquals(0, player.getNextWindowIndex()); - }, - - /** - * Tests navigation within the queue when repeat mode and shuffle mode is on. - */ - testRepeatMode_all_inShuffleMode() { - const initialOrder = [2, 1, 0]; - let shuffleOrder; - let windowIndex; - player.addQueueItems(0, util.queue.slice(0, 3), initialOrder); - player.prepare(); - player.addPlayerListener((state) => { - shuffleOrder = state.shuffleOrder; - windowIndex = state.windowIndex; - }); - player.setRepeatMode(Player.RepeatMode.ALL); - player.setShuffleModeEnabled(true); - assertEquals(windowIndex, player.shuffleOrder_[player.shuffleIndex_]); - assertArrayEquals(initialOrder, shuffleOrder); - - player.seekToWindow(shuffleOrder[2], 0); - assertEquals(shuffleOrder[2], windowIndex); - assertEquals(shuffleOrder[0], player.getNextWindowIndex()); - assertEquals(shuffleOrder[1], player.getPreviousWindowIndex()); - - player.seekToWindow(shuffleOrder[0], 0); - assertEquals(shuffleOrder[0], windowIndex); - }, - - /** Tests the repeat mode 'one' */ - testRepeatMode_one() { - let repeatMode; - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.addPlayerListener((state) => { - repeatMode = state.repeatMode; - }); - player.setRepeatMode(Player.RepeatMode.ONE); - assertEquals(Player.RepeatMode.ONE, repeatMode); - assertEquals(0, player.getPreviousWindowIndex()); - assertEquals(0, player.getNextWindowIndex()); - - player.seekToWindow(1, 0); - assertEquals(1, player.getPreviousWindowIndex()); - assertEquals(1, player.getNextWindowIndex()); - - player.setShuffleModeEnabled(true); - assertEquals(1, player.getPreviousWindowIndex()); - assertEquals(1, player.getNextWindowIndex()); - }, - - /** Tests building a media item info from the manifest. */ - testBuildMediaItemInfo_fromManifest() { - let mediaItemInfos = null; - player.addQueueItems(0, util.queue.slice(0, 3)); - player.addPlayerListener((state) => { - mediaItemInfos = state.mediaItemsInfo; - }); - player.seekToWindow(1, 0); - player.prepare(); - assertUndefined(mediaItemInfos['uuid0']); - const mediaItemInfo = mediaItemInfos['uuid1']; - assertNotUndefined(mediaItemInfo); - assertFalse(mediaItemInfo.isDynamic); - assertTrue(mediaItemInfo.isSeekable); - assertEquals(0, mediaItemInfo.defaultStartPositionUs); - assertEquals(20 * 1000 * 1000, mediaItemInfo.windowDurationUs); - assertEquals(1, mediaItemInfo.periods.length); - assertEquals(20 * 1000 * 1000, mediaItemInfo.periods[0].durationUs); - }, - - /** Tests building a media item info with multiple periods. */ - testBuildMediaItemInfo_fromManifest_multiPeriod() { - let mediaItemInfos = null; - player.addQueueItems(0, util.queue.slice(0, 3)); - player.addPlayerListener((state) => { - mediaItemInfos = state.mediaItemsInfo; - }); - // Setting manifest properties to emulate a multiperiod stream manifest. - mocks.state().getManifest().periods.push({startTime: 20}); - mocks.state().manifestState.windowDuration = 50; - player.seekToWindow(1, 0); - player.prepare(); - - const mediaItemInfo = mediaItemInfos['uuid1']; - assertNotUndefined(mediaItemInfo); - assertFalse(mediaItemInfo.isDynamic); - assertTrue(mediaItemInfo.isSeekable); - assertEquals(0, mediaItemInfo.defaultStartPositionUs); - assertEquals(50 * 1000 * 1000, mediaItemInfo.windowDurationUs); - assertEquals(2, mediaItemInfo.periods.length); - assertEquals(20 * 1000 * 1000, mediaItemInfo.periods[0].durationUs); - assertEquals(30 * 1000 * 1000, mediaItemInfo.periods[1].durationUs); - }, - - /** Tests building a media item info from a live manifest. */ - testBuildMediaItemInfo_fromManifest_live() { - let mediaItemInfos = null; - player.addQueueItems(0, util.queue.slice(0, 3)); - player.addPlayerListener((state) => { - mediaItemInfos = state.mediaItemsInfo; - }); - // Setting manifest properties to emulate a live stream manifest. - mocks.state().manifestState.isLive = true; - mocks.state().manifestState.windowDuration = 30; - mocks.state().manifestState.delay = 10; - mocks.state().getManifest().periods.push({startTime: 20}); - player.seekToWindow(1, 0); - player.prepare(); - - const mediaItemInfo = mediaItemInfos['uuid1']; - assertNotUndefined(mediaItemInfo); - assertTrue(mediaItemInfo.isDynamic); - assertTrue(mediaItemInfo.isSeekable); - assertEquals(20 * 1000 * 1000, mediaItemInfo.defaultStartPositionUs); - assertEquals(20 * 1000 * 1000, mediaItemInfo.windowDurationUs); - assertEquals(2, mediaItemInfo.periods.length); - assertEquals(20 * 1000 * 1000, mediaItemInfo.periods[0].durationUs); - assertEquals(Infinity, mediaItemInfo.periods[1].durationUs); - }, - - /** Tests whether the shaka request filter is set for life streams. */ - testRequestFilterIsSetAndRemovedForLive() { - player.addQueueItems(0, util.queue.slice(0, 3)); - - // Set manifest properties to emulate a live stream manifest. - mocks.state().manifestState.isLive = true; - mocks.state().manifestState.windowDuration = 30; - mocks.state().manifestState.delay = 10; - mocks.state().getManifest().periods.push({startTime: 20}); - - assertNull(mocks.state().responseFilter); - assertFalse(player.isManifestFilterRegistered_); - player.seekToWindow(1, 0); - player.prepare(); - assertNotNull(mocks.state().responseFilter); - assertTrue(player.isManifestFilterRegistered_); - - // Set manifest properties to emulate a non-live stream */ - mocks.state().manifestState.isLive = false; - mocks.state().manifestState.windowDuration = 20; - mocks.state().manifestState.delay = 0; - mocks.state().getManifest().periods.push({startTime: 20}); - - player.seekToWindow(0, 0); - assertNull(mocks.state().responseFilter); - assertFalse(player.isManifestFilterRegistered_); - }, - - /** Tests whether the media info is removed when queue item is removed. */ - testRemoveMediaItemInfo() { - let mediaItemInfos = null; - player.addQueueItems(0, util.queue.slice(0, 3)); - player.addPlayerListener((state) => { - mediaItemInfos = state.mediaItemsInfo; - }); - player.seekToWindow(1, 0); - player.prepare(); - assertNotUndefined(mediaItemInfos['uuid1']); - player.removeQueueItems(['uuid1']); - assertUndefined(mediaItemInfos['uuid1']); - }, - - /** Tests shuffling. */ - testSetShuffeModeEnabled() { - let shuffleModeEnabled = false; - player.addQueueItems(0, util.queue.slice(0, 3)); - player.addPlayerListener((state) => { - shuffleModeEnabled = state.shuffleModeEnabled; - }); - player.setShuffleModeEnabled(true); - assertTrue(shuffleModeEnabled); - - player.setShuffleModeEnabled(false); - assertFalse(shuffleModeEnabled); - }, - - /** Tests setting a new playback order. */ - async testSetShuffleOrder() { - const defaultOrder = [0, 1, 2]; - let shuffleOrder; - player.addPlayerListener((state) => { - shuffleOrder = state.shuffleOrder; - }); - await player.addQueueItems(0, util.queue.slice(0, 3), defaultOrder); - assertArrayEquals(defaultOrder, shuffleOrder); - - player.setShuffleOrder_([2, 1, 0]); - assertArrayEquals([2, 1, 0], player.shuffleOrder_); - }, - - /** Tests setting a new playback order with incorrect length. */ - async testSetShuffleOrder_incorrectLength() { - const defaultOrder = [0, 1, 2]; - let shuffleOrder; - player.addPlayerListener((state) => { - shuffleOrder = state.shuffleOrder; - }); - await player.addQueueItems(0, util.queue.slice(0, 3), defaultOrder); - assertArrayEquals(defaultOrder, shuffleOrder); - - shuffleOrder = undefined; - player.setShuffleOrder_([2, 1]); - assertUndefined(shuffleOrder); - }, - - /** Tests falling into ENDED when prepared with empty queue. */ - testPrepare_withEmptyQueue() { - player.seekToUuid('uuid1000', 1000); - assertEquals('uuid1000', player.uuidToPrepare_); - player.prepare(); - assertEquals('ENDED', player.getPlaybackState()); - assertNull(player.uuidToPrepare_); - player.seekToUuid('uuid1000', 1000); - assertNull(player.uuidToPrepare_); - }, -}); diff --git a/cast_receiver_app/test/queue_test.js b/cast_receiver_app/test/queue_test.js deleted file mode 100644 index b46361fb2e..0000000000 --- a/cast_receiver_app/test/queue_test.js +++ /dev/null @@ -1,166 +0,0 @@ -/** - * 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. - * - * @fileoverview Unit tests for queue manipulations. - */ - -goog.module('exoplayer.cast.test.queue'); -goog.setTestOnly(); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const Player = goog.require('exoplayer.cast.Player'); -const mocks = goog.require('exoplayer.cast.test.mocks'); -const testSuite = goog.require('goog.testing.testSuite'); -const util = goog.require('exoplayer.cast.test.util'); - -let player; - -testSuite({ - setUp() { - mocks.setUp(); - player = new Player(mocks.createShakaFake(), new ConfigurationFactory()); - }, - - /** Tests adding queue items. */ - testAddQueueItem() { - let queue = []; - player.addPlayerListener((state) => { - queue = state.mediaQueue; - }); - assertEquals(0, queue.length); - player.addQueueItems(0, util.queue.slice(0, 3)); - assertEquals(util.queue[0].media.uri, queue[0].media.uri); - assertEquals(util.queue[1].media.uri, queue[1].media.uri); - assertEquals(util.queue[2].media.uri, queue[2].media.uri); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests that duplicate queue items are ignored. */ - testAddDuplicateQueueItem() { - let queue = []; - player.addPlayerListener((state) => { - queue = state.mediaQueue; - }); - assertEquals(0, queue.length); - // Insert three items. - player.addQueueItems(0, util.queue.slice(0, 3)); - // Insert two of which the first is a duplicate. - player.addQueueItems(1, util.queue.slice(2, 4)); - assertEquals(4, queue.length); - assertArrayEquals( - ['uuid0', 'uuid3', 'uuid1', 'uuid2'], queue.slice().map((i) => i.uuid)); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests moving queue items. */ - testMoveQueueItem() { - const shuffleOrder = [0, 2, 1]; - let queue = []; - player.addPlayerListener((state) => { - queue = state.mediaQueue; - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.moveQueueItem('uuid0', 1, shuffleOrder); - assertEquals(util.queue[1].media.uri, queue[0].media.uri); - assertEquals(util.queue[0].media.uri, queue[1].media.uri); - assertEquals(util.queue[2].media.uri, queue[2].media.uri); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - - queue = undefined; - // invalid to index - player.moveQueueItem('uuid0', 11, [0, 1, 2]); - assertTrue(typeof queue === 'undefined'); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); - // negative to index - player.moveQueueItem('uuid0', -11, shuffleOrder); - assertTrue(typeof queue === 'undefined'); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); - // unknown uuid - player.moveQueueItem('unknown', 1, shuffleOrder); - assertTrue(typeof queue === 'undefined'); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); - }, - - /** Tests removing queue items. */ - testRemoveQueueItems() { - let queue = []; - player.addPlayerListener((state) => { - queue = state.mediaQueue; - }); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - player.prepare(); - player.seekToWindow(1, 0); - assertEquals(1, player.getCurrentWindowIndex()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); - - // Remove the first item. - player.removeQueueItems(['uuid0']); - assertEquals(2, queue.length); - assertEquals(util.queue[1].media.uri, queue[0].media.uri); - assertEquals(util.queue[2].media.uri, queue[1].media.uri); - assertEquals(0, player.getCurrentWindowIndex()); - assertArrayEquals([1,0], player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); - - // Calling stop without reseting preserves the queue. - player.stop(false); - assertEquals('uuid1', player.uuidToPrepare_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); - - // Remove the item at the end of the queue. - player.removeQueueItems(['uuid2']); - util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); - - // Remove the last remaining item in the queue. - player.removeQueueItems(['uuid1']); - assertEquals(0, queue.length); - assertEquals('IDLE', player.getPlaybackState()); - assertEquals(0, player.getCurrentWindowIndex()); - assertArrayEquals([], player.shuffleOrder_); - assertNull(player.uuidToPrepare_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); - }, - - /** Tests removing multiple unordered queue items at once. */ - testRemoveQueueItems_multiple() { - let queue = []; - player.addPlayerListener((state) => { - queue = state.mediaQueue; - }); - player.addQueueItems(0, util.queue.slice(0, 6), []); - player.prepare(); - - assertEquals(6, queue.length); - player.removeQueueItems(['uuid1', 'uuid5', 'uuid3']); - assertArrayEquals(['uuid0', 'uuid2', 'uuid4'], queue.map((i) => i.uuid)); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests whether stopping with reset=true resets queue and uuidToIndexMap */ - testStop_resetTrue() { - let queue = []; - player.addPlayerListener((state) => { - queue = state.mediaQueue; - }); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - player.prepare(); - player.stop(true); - assertEquals(0, player.queue_.length); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, -}); diff --git a/cast_receiver_app/test/receiver_test.js b/cast_receiver_app/test/receiver_test.js deleted file mode 100644 index 303a1caf64..0000000000 --- a/cast_receiver_app/test/receiver_test.js +++ /dev/null @@ -1,1027 +0,0 @@ -/** - * 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. - * - * @fileoverview Unit tests for receiver. - */ - -goog.module('exoplayer.cast.test.receiver'); -goog.setTestOnly(); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher'); -const Player = goog.require('exoplayer.cast.Player'); -const Receiver = goog.require('exoplayer.cast.Receiver'); -const mocks = goog.require('exoplayer.cast.test.mocks'); -const testSuite = goog.require('goog.testing.testSuite'); -const util = goog.require('exoplayer.cast.test.util'); - -/** @type {?Player|undefined} */ -let player; -/** @type {!Array} */ -let queue = []; -let shakaFake; -let castContextMock; - -/** - * Sends a message to the receiver under test. - * - * @param {!Object} message The message to send as json. - */ -const sendMessage = function(message) { - mocks.state().customMessageListener({ - data: message, - senderId: 'sender0', - }); -}; - -/** - * Creates a valid media item with the suffix appended to each field. - * - * @param {string} suffix The suffix to append to the fields value. - * @return {!Object} The media item. - */ -const createMediaItem = function(suffix) { - return { - uuid: 'uuid' + suffix, - media: {uri: 'uri' + suffix}, - mimeType: 'application/dash+xml', - }; -}; - -let messageSequence = 0; - -/** - * Creates a message in the format sent bey the sender app. - * - * @param {string} method The name of the method. - * @param {?Object} args The arguments. - * @return {!Object} The message. - */ -const createMessage = function (method, args) { - return { - method: method, - args: args, - sequenceNumber: ++messageSequence, - }; -}; - -/** - * Asserts the `playerState` is in the same state as just after creation of the - * player. - * - * @param {!PlayerState} playerState The player state to assert. - * @param {string} playbackState The expected playback state. - */ -const assertInitialState = function(playerState, playbackState) { - assertEquals(playbackState, playerState.playbackState); - // Assert the state is in initial state. - assertArrayEquals([], queue); - assertEquals(0, playerState.windowCount); - assertEquals(0, playerState.windowIndex); - assertUndefined(playerState.playbackError); - assertNull(playerState.playbackPosition); - // Assert player properties. - assertEquals(0, player.getDurationMs()); - assertArrayEquals([], Object.entries(player.mediaItemInfoMap_)); - assertEquals(0, player.windowPeriodIndex_); - assertEquals(999, player.playbackType_); - assertEquals(0, player.getCurrentWindowIndex()); - assertEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); -}; - - -testSuite({ - setUp() { - mocks.setUp(); - shakaFake = mocks.createShakaFake(); - castContextMock = mocks.createCastReceiverContextFake(); - player = new Player(shakaFake, new ConfigurationFactory()); - player.addPlayerListener((playerState) => { - queue = playerState.mediaQueue; - }); - const messageDispatcher = new MessageDispatcher( - 'urn:x-cast:com.google.exoplayer.cast', castContextMock); - new Receiver(player, castContextMock, messageDispatcher); - }, - - tearDown() { - queue = []; - }, - - /** Tests whether a status was sent to the sender on connect. */ - testNotifyClientConnected() { - assertUndefined(mocks.state().outputMessages['sender0']); - - sendMessage(createMessage('player.onClientConnected', {})); - const message = mocks.state().outputMessages['sender0'][0]; - assertEquals(messageSequence, message.sequenceNumber); - }, - - /** - * Tests whether a custom message listener has been registered after - * construction. - */ - testCustomMessageListener() { - assertTrue(goog.isFunction(mocks.state().customMessageListener)); - }, - - /** Tests set playWhenReady. */ - testSetPlayWhenReady() { - let playWhenReady; - player.addPlayerListener((playerState) => { - playWhenReady = playerState.playWhenReady; - }); - - sendMessage(createMessage( - 'player.setPlayWhenReady', - { playWhenReady: true } - )); - assertTrue(playWhenReady); - sendMessage(createMessage( - 'player.setPlayWhenReady', - { playWhenReady: false } - )); - assertFalse(playWhenReady); - }, - - /** Tests setting repeat modes. */ - testSetRepeatMode() { - let repeatMode; - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.addPlayerListener((playerState) => { - repeatMode = playerState.repeatMode; - }); - - sendMessage(createMessage( - 'player.setRepeatMode', - { repeatMode: Player.RepeatMode.ONE } - )); - assertEquals(Player.RepeatMode.ONE, repeatMode); - assertEquals(0, player.getNextWindowIndex()); - assertEquals(0, player.getPreviousWindowIndex()); - - sendMessage(createMessage( - 'player.setRepeatMode', - { repeatMode: Player.RepeatMode.ALL } - )); - assertEquals(Player.RepeatMode.ALL, repeatMode); - assertEquals(1, player.getNextWindowIndex()); - assertEquals(2, player.getPreviousWindowIndex()); - - sendMessage(createMessage( - 'player.setRepeatMode', - { repeatMode: Player.RepeatMode.OFF } - )); - assertEquals(Player.RepeatMode.OFF, repeatMode); - assertEquals(1, player.getNextWindowIndex()); - assertTrue(player.getPreviousWindowIndex() < 0); - }, - - /** Tests setting an invalid repeat mode value. */ - testSetRepeatMode_invalid_noStateChange() { - let repeatMode; - player.addPlayerListener((playerState) => { - repeatMode = playerState.repeatMode; - }); - - sendMessage(createMessage( - 'player.setRepeatMode', - { repeatMode: "UNKNOWN" } - )); - assertEquals(Player.RepeatMode.OFF, player.repeatMode_); - assertUndefined(repeatMode); - player.invalidate(); - assertEquals(Player.RepeatMode.OFF, repeatMode); - }, - - /** Tests enabling and disabling shuffle mode. */ - testSetShuffleModeEnabled() { - const enableMessage = createMessage('player.setShuffleModeEnabled', { - shuffleModeEnabled: true, - }); - const disableMessage = createMessage('player.setShuffleModeEnabled', { - shuffleModeEnabled: false, - }); - let shuffleModeEnabled; - player.addPlayerListener((state) => { - shuffleModeEnabled = state.shuffleModeEnabled; - }); - assertFalse(player.shuffleModeEnabled_); - sendMessage(enableMessage); - assertTrue(shuffleModeEnabled); - sendMessage(disableMessage); - assertFalse(shuffleModeEnabled); - }, - - /** Tests adding a single media item to the queue. */ - testAddMediaItem_single() { - const suffix = '0'; - const jsonMessage = createMessage('player.addItems', { - index: 0, - items: [ - createMediaItem(suffix), - ], - shuffleOrder: [0], - }); - - sendMessage(jsonMessage); - assertEquals(1, queue.length); - assertEquals('uuid0', queue[0].uuid); - assertEquals('uri0', queue[0].media.uri); - assertArrayEquals([0], player.shuffleOrder_); - }, - - /** Tests adding multiple media items to the queue. */ - testAddMediaItem_multiple() { - const shuffleOrder = [0, 2, 1]; - const jsonMessage = createMessage('player.addItems', { - index: 0, - items: [ - createMediaItem('0'), - createMediaItem('1'), - createMediaItem('2'), - ], - shuffleOrder: shuffleOrder, - }); - - sendMessage(jsonMessage); - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - }, - - /** Tests adding a media item to end of the queue by omitting the index. */ - testAddMediaItem_noindex_addstoend() { - const shuffleOrder = [1, 3, 2, 0]; - const jsonMessage = createMessage('player.addItems', { - items: [createMediaItem('99')], - shuffleOrder: shuffleOrder, - }); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - let queue = []; - player.addPlayerListener((playerState) => { - queue = playerState.mediaQueue; - }); - sendMessage(jsonMessage); - assertEquals(4, queue.length); - assertEquals('uuid99', queue[3].uuid); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - }, - - /** Tests adding items with a shuffle order of invalid length. */ - testAddMediaItems_invalidShuffleOrderLength() { - const shuffleOrder = [1, 3, 2]; - const jsonMessage = createMessage('player.addItems', { - items: [createMediaItem('99')], - shuffleOrder: shuffleOrder, - }); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - let queue = []; - player.addPlayerListener((playerState) => { - queue = playerState.mediaQueue; - }); - sendMessage(jsonMessage); - assertEquals(4, queue.length); - assertEquals('uuid99', queue[3].uuid); - assertEquals(4, player.shuffleOrder_.length); - }, - - /** Tests inserting a media item to the queue. */ - testAddMediaItem_insert() { - const index = 1; - const shuffleOrder = [1, 0, 3, 2, 4]; - const firstInsertionMessage = createMessage('player.addItems', { - index, - items: [ - createMediaItem('99'), - createMediaItem('100'), - ], - shuffleOrder, - }); - const prepareMessage = createMessage('player.prepare', {}); - const secondInsertionMessage = createMessage('player.addItems', { - index, - items: [ - createMediaItem('199'), - createMediaItem('1100'), - ], - shuffleOrder, - }); - // fill with three items - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - player.seekToUuid('uuid99', 0); - - sendMessage(firstInsertionMessage); - // The window index does not change when IDLE. - assertEquals(1, player.getCurrentWindowIndex()); - assertEquals(5, queue.length); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - - // Prepare sets the index by the uuid to which we seeked. - sendMessage(prepareMessage); - assertEquals(1, player.getCurrentWindowIndex()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - // Add two items at the current window index. - sendMessage(secondInsertionMessage); - // Current window index is adjusted. - assertEquals(3, player.getCurrentWindowIndex()); - assertEquals(7, queue.length); - assertEquals('uuid199', queue[index].uuid); - assertEquals(7, player.shuffleOrder_.length); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests adding a media item with an index larger than the queue size. */ - testAddMediaItem_indexLargerThanQueueSize_addsToEnd() { - const index = 4; - const jsonMessage = createMessage('player.addItems', { - index: index, - items: [ - createMediaItem('99'), - createMediaItem('100'), - ], - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid99', 'uuid100'], - queue.map((x) => x.uuid)); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing an item from the queue. */ - testRemoveMediaItem() { - const jsonMessage = - createMessage('player.removeItems', {uuids: ['uuid1', 'uuid0']}); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); - - sendMessage(jsonMessage); - assertArrayEquals(['uuid2'], queue.map((x) => x.uuid)); - assertArrayEquals([0], player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing the currently playing item from the queue. */ - async testRemoveMediaItem_currentItem() { - const jsonMessage = - createMessage('player.removeItems', {uuids: ['uuid1', 'uuid0']}); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - player.seekToWindow(1, 0); - player.prepare(); - - await sendMessage(jsonMessage); - assertArrayEquals(['uuid2'], queue.map((x) => x.uuid)); - assertEquals(0, player.getCurrentWindowIndex()); - assertEquals(util.queue[2].media.uri, shakaFake.getMediaElement().src); - assertArrayEquals([0], player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing items which affect the current window index. */ - async testRemoveMediaItem_affectsWindowIndex() { - const jsonMessage = - createMessage('player.removeItems', {uuids: ['uuid1', 'uuid0']}); - const currentUri = util.queue[4].media.uri; - player.addQueueItems(0, util.queue.slice(0, 6), [3, 2, 1, 4, 0, 5]); - player.prepare(); - await player.seekToWindow(4, 2000); - assertEquals(currentUri, shakaFake.getMediaElement().src); - - sendMessage(jsonMessage); - assertEquals(4, queue.length); - assertEquals('uuid4', queue[player.getCurrentWindowIndex()].uuid); - assertEquals(2, player.getCurrentWindowIndex()); - assertEquals(currentUri, shakaFake.getMediaElement().src); - assertArrayEquals([1, 0, 2, 3], player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing the last item of the queue. */ - testRemoveMediaItem_firstItem_windowIndexIsCorrect() { - const jsonMessage = - createMessage('player.removeItems', {uuids: ['uuid0']}); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - player.seekToWindow(1, 0); - - sendMessage(jsonMessage); - assertArrayEquals(['uuid1', 'uuid2'], queue.map((x) => x.uuid)); - assertEquals(0, player.getCurrentWindowIndex()); - assertArrayEquals([1, 0], player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing the last item of the queue. */ - testRemoveMediaItem_lastItem_windowIndexIsCorrect() { - const jsonMessage = - createMessage('player.removeItems', {uuids: ['uuid2']}); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - player.seekToWindow(2, 0); - player.prepare(); - - mocks.state().isSilent = true; - const states = []; - player.addPlayerListener((playerState) => { - states.push(playerState); - }); - sendMessage(jsonMessage); - assertArrayEquals(['uuid0', 'uuid1'], queue.map((x) => x.uuid)); - assertEquals(1, player.getCurrentWindowIndex()); - assertArrayEquals([0, 1], player.shuffleOrder_); - assertEquals(1, states.length); - assertEquals(Player.PlaybackState.BUFFERING, states[0].playbackState); - assertEquals( - Player.DiscontinuityReason.PERIOD_TRANSITION, - states[0].playbackPosition.discontinuityReason); - assertEquals( - Player.DUMMY_MEDIA_ITEM_INFO, states[0].mediaItemsInfo['uuid1']); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing items all items. */ - testRemoveMediaItem_removeAll() { - const jsonMessage = createMessage('player.removeItems', - {uuids: ['uuid1', 'uuid0', 'uuid2']}); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.seekToWindow(2, 2000); - player.prepare(); - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - - sendMessage(jsonMessage); - assertInitialState(playerState, 'ENDED'); - assertEquals(0, player.getCurrentWindowIndex()); - assertArrayEquals([], player.shuffleOrder_); - assertEquals(Player.PlaybackState.ENDED, player.getPlaybackState()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, []); - }, - - /** Tests moving an item in the queue. */ - testMoveItem() { - let shuffleOrder = [0, 2, 1]; - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid2', - index: 0, - shuffleOrder: shuffleOrder, - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid2', 'uuid0', 'uuid1'], queue.map((x) => x.uuid)); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests moving the currently playing item in the queue. */ - testMoveItem_currentWindowIndex() { - let shuffleOrder = [0, 2, 1]; - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid2', - index: 0, - shuffleOrder: shuffleOrder, - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.seekToUuid('uuid2', 0); - assertEquals(2, player.getCurrentWindowIndex()); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid2', 'uuid0', 'uuid1'], queue.map((x) => x.uuid)); - assertEquals(0, player.getCurrentWindowIndex()); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests moving an item from before to after the currently playing item. */ - testMoveItem_decreaseCurrentWindowIndex() { - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid0', - index: 5, - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 6)); - player.prepare(); - player.seekToWindow(2, 0); - assertEquals(2, player.getCurrentWindowIndex()); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], - queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5', 'uuid0'], - queue.map((x) => x.uuid)); - assertEquals(1, player.getCurrentWindowIndex()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests moving an item from after to before the currently playing item. */ - testMoveItem_increaseCurrentWindowIndex() { - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid5', - index: 0, - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 6)); - player.prepare(); - player.seekToWindow(2, 0); - assertEquals(2, player.getCurrentWindowIndex()); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], - queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid5', 'uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4'], - queue.map((x) => x.uuid)); - assertEquals(3, player.getCurrentWindowIndex()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests moving an item from after to the current window index. */ - testMoveItem_toCurrentWindowIndex_fromAfter() { - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid5', - index: 2, - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 6)); - player.prepare(); - player.seekToWindow(2, 0); - assertEquals(2, player.getCurrentWindowIndex()); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], - queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid0', 'uuid1', 'uuid5', 'uuid2', 'uuid3', 'uuid4'], - queue.map((x) => x.uuid)); - assertEquals(3, player.getCurrentWindowIndex()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests moving an item from before to the current window index. */ - testMoveItem_toCurrentWindowIndex_fromBefore() { - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid0', - index: 2, - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 6)); - player.prepare(); - player.seekToWindow(2, 0); - assertEquals(2, player.getCurrentWindowIndex()); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], - queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid1', 'uuid2', 'uuid0', 'uuid3', 'uuid4', 'uuid5'], - queue.map((x) => x.uuid)); - assertEquals(1, player.getCurrentWindowIndex()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests seekTo. */ - testSeekTo() { - const jsonMessage = createMessage('player.seekTo', - { - 'uuid': 'uuid1', - 'positionMs': 2000 - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - sendMessage(jsonMessage); - assertEquals(2000, player.getCurrentPositionMs()); - assertEquals(1, player.getCurrentWindowIndex()); - }, - - /** Tests seekTo to unknown uuid. */ - testSeekTo_unknownUuid() { - const jsonMessage = createMessage('player.seekTo', - { - 'uuid': 'unknown', - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.seekToWindow(1, 2000); - assertEquals(2000, player.getCurrentPositionMs()); - assertEquals(1, player.getCurrentWindowIndex()); - - sendMessage(jsonMessage); - assertEquals(2000, player.getCurrentPositionMs()); - assertEquals(1, player.getCurrentWindowIndex()); - }, - - /** Tests seekTo without position. */ - testSeekTo_noPosition_defaultsToZero() { - const jsonMessage = createMessage('player.seekTo', - { - 'uuid': 'uuid1', - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - sendMessage(jsonMessage); - assertEquals(0, player.getCurrentPositionMs()); - assertEquals(1, player.getCurrentWindowIndex()); - }, - - /** Tests seekTo to negative position. */ - testSeekTo_negativePosition_defaultsToZero() { - const jsonMessage = createMessage('player.seekTo', - { - 'uuid': 'uuid2', - 'positionMs': -1, - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.seekToWindow(1, 2000); - assertEquals(2000, player.getCurrentPositionMs()); - assertEquals(1, player.getCurrentWindowIndex()); - - sendMessage(jsonMessage); - assertEquals(0, player.getCurrentPositionMs()); - assertEquals(2, player.getCurrentWindowIndex()); - }, - - /** Tests whether validation is turned on. */ - testMediaItemValidation_isOn() { - const index = 0; - const mediaItem = createMediaItem('99'); - delete mediaItem.uuid; - const jsonMessage = createMessage('player.addItems', { - index: index, - items: [mediaItem], - shuffleOrder: [], - }); - - sendMessage(jsonMessage); - assertEquals(0, queue.length); - }, - - /** Tests whether the state is sent to sender apps on state transition. */ - testPlayerStateIsSent_withCorrectSequenceNumber() { - assertUndefined(mocks.state().outputMessages['sender0']); - const playMessage = - createMessage('player.setPlayWhenReady', {playWhenReady: true}); - sendMessage(playMessage); - - const playerState = mocks.state().outputMessages['sender0'][0]; - assertTrue(playerState.playWhenReady); - assertEquals(playMessage.sequenceNumber, playerState.sequenceNumber); - }, - - /** Tests whether a connect of a sender app sends the current player state. */ - testSenderConnection() { - const onSenderConnected = mocks.state().onSenderConnected; - assertTrue(goog.isFunction(onSenderConnected)); - onSenderConnected({senderId: 'sender0'}); - - const playerState = mocks.state().outputMessages['sender0'][0]; - assertEquals(Player.RepeatMode.OFF, playerState.repeatMode); - assertEquals('IDLE', playerState.playbackState); - assertArrayEquals([], playerState.mediaQueue); - assertEquals(-1, playerState.sequenceNumber); - }, - - /** Tests whether a disconnect of a sender notifies the message dispatcher. */ - testSenderDisconnection_callsMessageDispatcher() { - mocks.setUp(); - let notifiedSenderId; - const myPlayer = new Player(mocks.createShakaFake()); - const myManagerFake = mocks.createCastReceiverContextFake(); - new Receiver(myPlayer, myManagerFake, { - registerActionHandler() {}, - notifySenderDisconnected(senderId) { - notifiedSenderId = senderId; - }, - }); - - const onSenderDisconnected = mocks.state().onSenderDisconnected; - assertTrue(goog.isFunction(onSenderDisconnected)); - onSenderDisconnected({senderId: 'sender0'}); - assertEquals('sender0', notifiedSenderId); - }, - - /** - * Tests whether the state right after creation of the player matches - * expectations. - */ - testInitialState() { - mocks.state().isSilent = true; - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - assertEquals(0, player.getCurrentPositionMs()); - // Dump a player state to the listener. - player.invalidate(); - // Asserts the state just after creation. - assertInitialState(playerState, 'IDLE'); - }, - - /** Tests whether user properties can be changed when in IDLE state */ - testChangingUserPropertiesWhenIdle() { - mocks.state().isSilent = true; - const states = []; - let counter = 0; - player.addPlayerListener((state) => { - states.push(state); - }); - // Adding items when IDLE. - player.addQueueItems(0, util.queue.slice(0, 3)); - counter++; - assertEquals(counter, states.length); - assertEquals(Player.PlaybackState.IDLE, states[counter - 1].playbackState); - assertArrayEquals( - ['uuid0', 'uuid1', 'uuid2'], - states[counter - 1].mediaQueue.map((i) => i.uuid)); - - // Set playWhenReady when IDLE. - assertFalse(player.getPlayWhenReady()); - player.setPlayWhenReady(true); - counter++; - assertTrue(player.getPlayWhenReady()); - assertEquals(counter, states.length); - assertEquals(Player.PlaybackState.IDLE, states[counter - 1].playbackState); - - // Seeking when IDLE. - player.seekToUuid('uuid2', 1000); - counter++; - // Window index not set when idle. - assertEquals(2, player.getCurrentWindowIndex()); - assertEquals(1000, player.getCurrentPositionMs()); - assertEquals(counter, states.length); - assertEquals(Player.PlaybackState.IDLE, states[counter - 1].playbackState); - // But window index is set when prepared. - player.prepare(); - assertEquals(2, player.getCurrentWindowIndex()); - }, - - /** Tests the state after calling prepare. */ - testPrepare() { - mocks.state().isSilent = true; - const states = []; - let counter = 0; - player.addPlayerListener((state) => { - states.push(state); - }); - const prepareMessage = createMessage('player.prepare', {}); - - player.addQueueItems(0, util.queue.slice(0, 3)); - player.seekToWindow(1, 1000); - counter += 2; - - // Sends prepare message. - sendMessage(prepareMessage); - counter++; - assertEquals(counter, states.length); - assertEquals('uuid1', states[counter - 1].playbackPosition.uuid); - assertEquals( - Player.PlaybackState.BUFFERING, states[counter - 1].playbackState); - - // Fakes Shaka events. - mocks.state().isSilent = false; - mocks.notifyListeners('streaming'); - mocks.notifyListeners('loadeddata'); - counter += 2; - assertEquals(counter, states.length); - assertEquals(Player.PlaybackState.READY, states[counter - 1].playbackState); - }, - - /** Tests stopping the player with `reset=true`. */ - testStop_resetTrue() { - mocks.state().isSilent = true; - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - const stopMessage = createMessage('player.stop', {reset: true}); - - player.setRepeatMode(Player.RepeatMode.ALL); - player.setShuffleModeEnabled(true); - player.setPlayWhenReady(true); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - mocks.state().isSilent = false; - mocks.notifyListeners('loadeddata'); - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((i) => i.uuid)); - assertEquals(0, playerState.windowIndex); - assertNotEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); - assertEquals(1, player.playbackType_); - // Stop the player. - sendMessage(stopMessage); - // Asserts the state looks the same as just after creation. - assertInitialState(playerState, 'IDLE'); - assertNull(playerState.playbackPosition); - // Assert player properties are preserved. - assertTrue(playerState.shuffleModeEnabled); - assertTrue(playerState.playWhenReady); - assertEquals(Player.RepeatMode.ALL, playerState.repeatMode); - }, - - /** Tests stopping the player with `reset=false`. */ - testStop_resetFalse() { - mocks.state().isSilent = true; - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - const stopMessage = createMessage('player.stop', {reset: false}); - - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.seekToUuid('uuid1', 1000); - mocks.state().isSilent = false; - mocks.notifyListeners('streaming'); - mocks.notifyListeners('trackschanged'); - mocks.notifyListeners('loadeddata'); - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((i) => i.uuid)); - assertEquals(1, playerState.windowIndex); - assertNotEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); - assertEquals(2, player.playbackType_); - // Stop the player. - sendMessage(stopMessage); - assertEquals('IDLE', playerState.playbackState); - assertUndefined(playerState.playbackError); - // Assert the timeline is preserved. - assertEquals(3, queue.length); - assertEquals(3, playerState.windowCount); - assertEquals(1, player.windowIndex_); - assertEquals(1, playerState.windowIndex); - // Assert the playback position is correct. - assertEquals(1000, playerState.playbackPosition.positionMs); - assertEquals('uuid1', playerState.playbackPosition.uuid); - assertEquals(0, playerState.playbackPosition.periodId); - assertNull(playerState.playbackPosition.discontinuityReason); - assertEquals(1000, player.getCurrentPositionMs()); - // Assert player properties are preserved. - assertEquals(20000, player.getDurationMs()); - assertEquals(2, Object.entries(player.mediaItemInfoMap_).length); - assertEquals(0, player.windowPeriodIndex_); - assertEquals(1, player.getCurrentWindowIndex()); - assertEquals(1, player.windowIndex_); - assertNotEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); - assertEquals(999, player.playbackType_); - assertEquals('uuid1', player.uuidToPrepare_); - }, - - /** - * Tests the state after having removed the last item in the queue. This - * resolves to the same state like calling `stop(true)` except that the state - * is ENDED and the queue is naturally empty and hence the windowIndex is - * unset. - */ - testRemoveLastQueueItem() { - mocks.state().isSilent = true; - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - const removeAllItemsMessage = createMessage( - 'player.removeItems', {uuids: ['uuid0', 'uuid1', 'uuid2']}); - - player.addQueueItems(0, util.queue.slice(0, 3)); - player.seekToWindow(0, 1000); - player.prepare(); - mocks.state().isSilent = false; - mocks.notifyListeners('loadeddata'); - // Remove all items. - sendMessage(removeAllItemsMessage); - // Assert the state after removal of all items. - assertInitialState(playerState, 'ENDED'); - }, - - /** Tests whether a player state is sent when no item is added. */ - testAddItem_noop() { - mocks.state().isSilent = true; - let playerStates = []; - player.addPlayerListener((state) => { - playerStates.push(state); - }); - const noOpMessage = createMessage('player.addItems', { - index: 0, - items: [ - util.queue[0], - ], - shuffleOrder: [0], - }); - player.addQueueItems(0, [util.queue[0]], []); - player.prepare(); - assertEquals(2, playerStates.length); - assertEquals(2, mocks.state().outputMessages['sender0'].length); - sendMessage(noOpMessage); - assertEquals(2, playerStates.length); - assertEquals(3, mocks.state().outputMessages['sender0'].length); - }, - - /** Tests whether a player state is sent when no item is removed. */ - testRemoveItem_noop() { - mocks.state().isSilent = true; - let playerStates = []; - player.addPlayerListener((state) => { - playerStates.push(state); - }); - const noOpMessage = - createMessage('player.removeItems', {uuids: ['uuid00']}); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - assertEquals(2, playerStates.length); - assertEquals(2, mocks.state().outputMessages['sender0'].length); - sendMessage(noOpMessage); - assertEquals(2, playerStates.length); - assertEquals(3, mocks.state().outputMessages['sender0'].length); - }, - - /** Tests whether a player state is sent when item is not moved. */ - testMoveItem_noop() { - mocks.state().isSilent = true; - let playerStates = []; - player.addPlayerListener((state) => { - playerStates.push(state); - }); - const noOpMessage = createMessage('player.moveItem', { - uuid: 'uuid00', - index: 0, - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - assertEquals(2, playerStates.length); - assertEquals(2, mocks.state().outputMessages['sender0'].length); - sendMessage(noOpMessage); - assertEquals(2, playerStates.length); - assertEquals(3, mocks.state().outputMessages['sender0'].length); - }, - - /** Tests whether playback actions send a state when no-op */ - testNoOpPlaybackActionsSendPlayerState() { - mocks.state().isSilent = true; - let playerStates = []; - let parsedMessage; - player.addPlayerListener((state) => { - playerStates.push(state); - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - - const outputMessages = mocks.state().outputMessages['sender0']; - const setupMessageCount = playerStates.length; - let totalMessageCount = setupMessageCount; - assertEquals(setupMessageCount, playerStates.length); - assertEquals(totalMessageCount, outputMessages.length); - - const firstNoOpMessage = createMessage('player.setPlayWhenReady', { - playWhenReady: false, - }); - let expectedSequenceNumber = firstNoOpMessage.sequenceNumber; - - sendMessage(firstNoOpMessage); - totalMessageCount++; - assertEquals(setupMessageCount, playerStates.length); - assertEquals(totalMessageCount, outputMessages.length); - parsedMessage = outputMessages[totalMessageCount - 1]; - assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); - - sendMessage(createMessage('player.setRepeatMode', { - repeatMode: 'OFF', - })); - totalMessageCount++; - assertEquals(setupMessageCount, playerStates.length); - assertEquals(totalMessageCount, outputMessages.length); - parsedMessage = outputMessages[totalMessageCount - 1]; - assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); - - sendMessage(createMessage('player.setShuffleModeEnabled', { - shuffleModeEnabled: false, - })); - totalMessageCount++; - assertEquals(setupMessageCount, playerStates.length); - assertEquals(totalMessageCount, outputMessages.length); - parsedMessage = outputMessages[totalMessageCount - 1]; - assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); - - sendMessage(createMessage('player.seekTo', { - uuid: 'not_existing', - positionMs: 0, - })); - totalMessageCount++; - assertEquals(setupMessageCount, playerStates.length); - assertEquals(totalMessageCount, outputMessages.length); - parsedMessage = outputMessages[totalMessageCount - 1]; - assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); - }, -}); diff --git a/cast_receiver_app/test/shaka_error_handling_test.js b/cast_receiver_app/test/shaka_error_handling_test.js deleted file mode 100644 index a7dafd3176..0000000000 --- a/cast_receiver_app/test/shaka_error_handling_test.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * 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. - * - * @fileoverview Unit tests for playback methods. - */ - -goog.module('exoplayer.cast.test.shaka'); -goog.setTestOnly(); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const Player = goog.require('exoplayer.cast.Player'); -const mocks = goog.require('exoplayer.cast.test.mocks'); -const testSuite = goog.require('goog.testing.testSuite'); -const util = goog.require('exoplayer.cast.test.util'); - -let player; -let shakaFake; - -testSuite({ - setUp() { - mocks.setUp(); - shakaFake = mocks.createShakaFake(); - player = new Player(shakaFake, new ConfigurationFactory()); - }, - - /** Tests Shaka critical error handling on load. */ - async testShakaCriticalError_onload() { - mocks.state().isSilent = true; - mocks.state().setShakaThrowsOnLoad(true); - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - player.addQueueItems(0, util.queue.slice(0, 2)); - player.seekToUuid('uuid1', 2000); - player.setPlayWhenReady(true); - // Calling prepare triggers a critical Shaka error. - await player.prepare(); - // Assert player state after error. - assertEquals('IDLE', playerState.playbackState); - assertEquals(mocks.state().shakaError.category, playerState.error.category); - assertEquals(mocks.state().shakaError.code, playerState.error.code); - assertEquals( - 'loading failed for uri: http://example1.com', - playerState.error.message); - assertEquals(999, player.playbackType_); - // Assert player properties are preserved. - assertEquals(2000, player.getCurrentPositionMs()); - assertTrue(player.getPlayWhenReady()); - assertEquals(1, player.getCurrentWindowIndex()); - assertEquals(1, player.windowIndex_); - }, - - /** Tests Shaka critical error handling on unload. */ - async testShakaCriticalError_onunload() { - mocks.state().isSilent = true; - mocks.state().setShakaThrowsOnUnload(true); - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - player.addQueueItems(0, util.queue.slice(0, 2)); - player.setPlayWhenReady(true); - assertUndefined(player.videoElement_.src); - // Calling prepare triggers a critical Shaka error. - await player.prepare(); - // Assert player state after caught and ignored error. - await assertEquals('BUFFERING', playerState.playbackState); - assertEquals('http://example.com', player.videoElement_.src); - assertEquals(1, player.playbackType_); - }, -}); diff --git a/cast_receiver_app/test/util.js b/cast_receiver_app/test/util.js deleted file mode 100644 index 22244675b7..0000000000 --- a/cast_receiver_app/test/util.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * 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. - * - * @fileoverview Description of this file. - */ - -goog.module('exoplayer.cast.test.util'); -goog.setTestOnly(); - -/** - * The queue of sample media items - * - * @type {!Array} - */ -const queue = [ - { - uuid: 'uuid0', - media: { - uri: 'http://example.com', - }, - mimeType: 'video/*', - }, - { - uuid: 'uuid1', - media: { - uri: 'http://example1.com', - }, - mimeType: 'application/dash+xml', - }, - { - uuid: 'uuid2', - media: { - uri: 'http://example2.com', - }, - mimeType: 'video/*', - }, - { - uuid: 'uuid3', - media: { - uri: 'http://example3.com', - }, - mimeType: 'application/dash+xml', - }, - { - uuid: 'uuid4', - media: { - uri: 'http://example4.com', - }, - mimeType: 'video/*', - }, - { - uuid: 'uuid5', - media: { - uri: 'http://example5.com', - }, - mimeType: 'application/dash+xml', - }, -]; - -/** - * Asserts whether the map of uuids is complete and points to the correct - * indices. - * - * @param {!Object} uuidIndexMap The uuid to index map. - * @param {!Array} queue The media item queue. - */ -const assertUuidIndexMap = (uuidIndexMap, queue) => { - assertEquals(queue.length, Object.entries(uuidIndexMap).length); - queue.forEach((mediaItem, index) => { - assertEquals(uuidIndexMap[mediaItem.uuid], index); - }); -}; - -exports.queue = queue; -exports.assertUuidIndexMap = assertUuidIndexMap; diff --git a/cast_receiver_app/test/validation_test.js b/cast_receiver_app/test/validation_test.js deleted file mode 100644 index 8e58185cfa..0000000000 --- a/cast_receiver_app/test/validation_test.js +++ /dev/null @@ -1,266 +0,0 @@ -/** - * 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. - * - * @fileoverview Unit tests for queue manipulations. - */ - -goog.module('exoplayer.cast.test.validation'); -goog.setTestOnly(); - -const testSuite = goog.require('goog.testing.testSuite'); -const validation = goog.require('exoplayer.cast.validation'); - -/** - * Creates a sample drm media for validation tests. - * - * @return {!Object} A dummy media item with a drm scheme. - */ -const createDrmMedia = function() { - return { - uuid: 'string', - media: { - uri: 'string', - }, - mimeType: 'application/dash+xml', - drmSchemes: [ - { - uuid: 'string', - licenseServer: { - uri: 'string', - requestHeaders: { - 'string': 'string', - }, - }, - }, - ], - }; -}; - -testSuite({ - - /** Tests minimal valid media item. */ - testValidateMediaItem_minimal() { - const mediaItem = { - uuid: 'string', - media: { - uri: 'string', - }, - mimeType: 'application/dash+xml', - }; - assertTrue(validation.validateMediaItem(mediaItem)); - - const uuid = mediaItem.uuid; - delete mediaItem.uuid; - assertFalse(validation.validateMediaItem(mediaItem)); - mediaItem.uuid = uuid; - assertTrue(validation.validateMediaItem(mediaItem)); - - const mimeType = mediaItem.mimeType; - delete mediaItem.mimeType; - assertFalse(validation.validateMediaItem(mediaItem)); - mediaItem.mimeType = mimeType; - assertTrue(validation.validateMediaItem(mediaItem)); - - const media = mediaItem.media; - delete mediaItem.media; - assertFalse(validation.validateMediaItem(mediaItem)); - mediaItem.media = media; - assertTrue(validation.validateMediaItem(mediaItem)); - - const uri = mediaItem.media.uri; - delete mediaItem.media.uri; - assertFalse(validation.validateMediaItem(mediaItem)); - mediaItem.media.uri = uri; - assertTrue(validation.validateMediaItem(mediaItem)); - }, - - /** Tests media item drm property validation. */ - testValidateMediaItem_drmSchemes() { - const mediaItem = createDrmMedia(); - assertTrue(validation.validateMediaItem(mediaItem)); - - const uuid = mediaItem.drmSchemes[0].uuid; - delete mediaItem.drmSchemes[0].uuid; - assertFalse(validation.validateMediaItem(mediaItem)); - mediaItem.drmSchemes[0].uuid = uuid; - assertTrue(validation.validateMediaItem(mediaItem)); - - const licenseServer = mediaItem.drmSchemes[0].licenseServer; - delete mediaItem.drmSchemes[0].licenseServer; - assertFalse(validation.validateMediaItem(mediaItem)); - mediaItem.drmSchemes[0].licenseServer = licenseServer; - assertTrue(validation.validateMediaItem(mediaItem)); - - const uri = mediaItem.drmSchemes[0].licenseServer.uri; - delete mediaItem.drmSchemes[0].licenseServer.uri; - assertFalse(validation.validateMediaItem(mediaItem)); - mediaItem.drmSchemes[0].licenseServer.uri = uri; - assertTrue(validation.validateMediaItem(mediaItem)); - }, - - /** Tests validation of startPositionUs and endPositionUs. */ - testValidateMediaItem_endAndStartPositionUs() { - const mediaItem = createDrmMedia(); - - mediaItem.endPositionUs = 0; - mediaItem.startPositionUs = 120 * 1000; - assertTrue(validation.validateMediaItem(mediaItem)); - - mediaItem.endPositionUs = '0'; - assertFalse(validation.validateMediaItem(mediaItem)); - - mediaItem.endPositionUs = 0; - assertTrue(validation.validateMediaItem(mediaItem)); - - mediaItem.startPositionUs = true; - assertFalse(validation.validateMediaItem(mediaItem)); - }, - - /** Tests validation of the title. */ - testValidateMediaItem_title() { - const mediaItem = createDrmMedia(); - - mediaItem.title = '0'; - assertTrue(validation.validateMediaItem(mediaItem)); - - mediaItem.title = 0; - assertFalse(validation.validateMediaItem(mediaItem)); - }, - - /** Tests validation of the description. */ - testValidateMediaItem_description() { - const mediaItem = createDrmMedia(); - - mediaItem.description = '0'; - assertTrue(validation.validateMediaItem(mediaItem)); - - mediaItem.description = 0; - assertFalse(validation.validateMediaItem(mediaItem)); - }, - - /** Tests validating property of type string. */ - testValidateProperty_string() { - const obj = { - field: 'string', - }; - assertTrue(validation.validateProperty(obj, 'field', 'string')); - assertTrue(validation.validateProperty(obj, 'field', '?string')); - - obj.field = 0; - assertFalse(validation.validateProperty(obj, 'field', 'string')); - assertFalse(validation.validateProperty(obj, 'field', '?string')); - - obj.field = true; - assertFalse(validation.validateProperty(obj, 'field', 'string')); - assertFalse(validation.validateProperty(obj, 'field', '?string')); - - obj.field = {}; - assertFalse(validation.validateProperty(obj, 'field', 'string')); - assertFalse(validation.validateProperty(obj, 'field', '?string')); - - delete obj.field; - assertFalse(validation.validateProperty(obj, 'field', 'string')); - assertTrue(validation.validateProperty(obj, 'field', '?string')); - }, - - /** Tests validating property of type number. */ - testValidateProperty_number() { - const obj = { - field: 0, - }; - assertTrue(validation.validateProperty(obj, 'field', 'number')); - assertTrue(validation.validateProperty(obj, 'field', '?number')); - - obj.field = '0'; - assertFalse(validation.validateProperty(obj, 'field', 'number')); - assertFalse(validation.validateProperty(obj, 'field', '?number')); - - obj.field = true; - assertFalse(validation.validateProperty(obj, 'field', 'number')); - assertFalse(validation.validateProperty(obj, 'field', '?number')); - - obj.field = {}; - assertFalse(validation.validateProperty(obj, 'field', 'number')); - assertFalse(validation.validateProperty(obj, 'field', '?number')); - - delete obj.field; - assertFalse(validation.validateProperty(obj, 'field', 'number')); - assertTrue(validation.validateProperty(obj, 'field', '?number')); - }, - - /** Tests validating property of type boolean. */ - testValidateProperty_boolean() { - const obj = { - field: true, - }; - assertTrue(validation.validateProperty(obj, 'field', 'boolean')); - assertTrue(validation.validateProperty(obj, 'field', '?boolean')); - - obj.field = '0'; - assertFalse(validation.validateProperty(obj, 'field', 'boolean')); - assertFalse(validation.validateProperty(obj, 'field', '?boolean')); - - obj.field = 1000; - assertFalse(validation.validateProperty(obj, 'field', 'boolean')); - assertFalse(validation.validateProperty(obj, 'field', '?boolean')); - - obj.field = [true]; - assertFalse(validation.validateProperty(obj, 'field', 'boolean')); - assertFalse(validation.validateProperty(obj, 'field', '?boolean')); - - delete obj.field; - assertFalse(validation.validateProperty(obj, 'field', 'boolean')); - assertTrue(validation.validateProperty(obj, 'field', '?boolean')); - }, - - /** Tests validating property of type array. */ - testValidateProperty_array() { - const obj = { - field: [], - }; - assertTrue(validation.validateProperty(obj, 'field', 'Array')); - assertTrue(validation.validateProperty(obj, 'field', '?Array')); - - obj.field = '0'; - assertFalse(validation.validateProperty(obj, 'field', 'Array')); - assertFalse(validation.validateProperty(obj, 'field', '?Array')); - - obj.field = 1000; - assertFalse(validation.validateProperty(obj, 'field', 'Array')); - assertFalse(validation.validateProperty(obj, 'field', '?Array')); - - obj.field = true; - assertFalse(validation.validateProperty(obj, 'field', 'Array')); - assertFalse(validation.validateProperty(obj, 'field', '?Array')); - - delete obj.field; - assertFalse(validation.validateProperty(obj, 'field', 'Array')); - assertTrue(validation.validateProperty(obj, 'field', '?Array')); - }, - - /** Tests validating properties of type RepeatMode */ - testValidateProperty_repeatMode() { - const obj = { - off: 'OFF', - one: 'ONE', - all: 'ALL', - invalid: 'invalid', - }; - assertTrue(validation.validateProperty(obj, 'off', 'RepeatMode')); - assertTrue(validation.validateProperty(obj, 'one', 'RepeatMode')); - assertTrue(validation.validateProperty(obj, 'all', 'RepeatMode')); - assertFalse(validation.validateProperty(obj, 'invalid', 'RepeatMode')); - }, -}); diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java deleted file mode 100644 index bc38cbdb8a..0000000000 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java +++ /dev/null @@ -1,437 +0,0 @@ -/* - * Copyright (C) 2017 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.castdemo; - -import android.content.Context; -import android.net.Uri; -import androidx.annotation.Nullable; -import android.view.KeyEvent; -import android.view.View; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Player.DiscontinuityReason; -import com.google.android.exoplayer2.Player.EventListener; -import com.google.android.exoplayer2.Player.TimelineChangeReason; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.ext.cast.CastPlayer; -import com.google.android.exoplayer2.ext.cast.MediaItem; -import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; -import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.ui.PlayerControlView; -import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.gms.cast.MediaInfo; -import com.google.android.gms.cast.MediaMetadata; -import com.google.android.gms.cast.MediaQueueItem; -import com.google.android.gms.cast.framework.CastContext; -import java.util.ArrayList; -import org.json.JSONException; -import org.json.JSONObject; - -/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */ -/* package */ class DefaultReceiverPlayerManager - implements PlayerManager, EventListener, SessionAvailabilityListener { - - private static final String USER_AGENT = "ExoCastDemoPlayer"; - private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = - new DefaultHttpDataSourceFactory(USER_AGENT); - - private final PlayerView localPlayerView; - private final PlayerControlView castControlView; - private final SimpleExoPlayer exoPlayer; - private final CastPlayer castPlayer; - private final ArrayList mediaQueue; - private final Listener listener; - private final ConcatenatingMediaSource concatenatingMediaSource; - - private int currentItemIndex; - private Player currentPlayer; - - /** - * Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}. - * - * @param listener A {@link Listener} for queue position changes. - * @param localPlayerView The {@link PlayerView} for local playback. - * @param castControlView The {@link PlayerControlView} to control remote playback. - * @param context A {@link Context}. - * @param castContext The {@link CastContext}. - */ - public DefaultReceiverPlayerManager( - Listener listener, - PlayerView localPlayerView, - PlayerControlView castControlView, - Context context, - CastContext castContext) { - this.listener = listener; - this.localPlayerView = localPlayerView; - this.castControlView = castControlView; - mediaQueue = new ArrayList<>(); - currentItemIndex = C.INDEX_UNSET; - concatenatingMediaSource = new ConcatenatingMediaSource(); - - DefaultTrackSelector trackSelector = new DefaultTrackSelector(); - RenderersFactory renderersFactory = new DefaultRenderersFactory(context); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector); - exoPlayer.addListener(this); - localPlayerView.setPlayer(exoPlayer); - - castPlayer = new CastPlayer(castContext); - castPlayer.addListener(this); - castPlayer.setSessionAvailabilityListener(this); - castControlView.setPlayer(castPlayer); - - setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); - } - - // Queue manipulation methods. - - /** - * Plays a specified queue item in the current player. - * - * @param itemIndex The index of the item to play. - */ - @Override - public void selectQueueItem(int itemIndex) { - setCurrentItem(itemIndex, C.TIME_UNSET, true); - } - - /** Returns the index of the currently played item. */ - @Override - public int getCurrentItemIndex() { - return currentItemIndex; - } - - /** - * Appends {@code item} to the media queue. - * - * @param item The {@link MediaItem} to append. - */ - @Override - public void addItem(MediaItem item) { - mediaQueue.add(item); - concatenatingMediaSource.addMediaSource(buildMediaSource(item)); - if (currentPlayer == castPlayer) { - castPlayer.addItems(buildMediaQueueItem(item)); - } - } - - /** Returns the size of the media queue. */ - @Override - public int getMediaQueueSize() { - return mediaQueue.size(); - } - - /** - * Returns the item at the given index in the media queue. - * - * @param position The index of the item. - * @return The item at the given index in the media queue. - */ - @Override - public MediaItem getItem(int position) { - return mediaQueue.get(position); - } - - /** - * Removes the item at the given index from the media queue. - * - * @param item The item to remove. - * @return Whether the removal was successful. - */ - @Override - public boolean removeItem(MediaItem item) { - int itemIndex = mediaQueue.indexOf(item); - if (itemIndex == -1) { - return false; - } - concatenatingMediaSource.removeMediaSource(itemIndex); - if (currentPlayer == castPlayer) { - if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { - Timeline castTimeline = castPlayer.getCurrentTimeline(); - if (castTimeline.getPeriodCount() <= itemIndex) { - return false; - } - castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id); - } - } - mediaQueue.remove(itemIndex); - if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { - maybeSetCurrentItemAndNotify(C.INDEX_UNSET); - } else if (itemIndex < currentItemIndex) { - maybeSetCurrentItemAndNotify(currentItemIndex - 1); - } - return true; - } - - /** - * Moves an item within the queue. - * - * @param item The item to move. - * @param toIndex The target index of the item in the queue. - * @return Whether the item move was successful. - */ - @Override - public boolean moveItem(MediaItem item, int toIndex) { - int fromIndex = mediaQueue.indexOf(item); - if (fromIndex == -1) { - return false; - } - // Player update. - concatenatingMediaSource.moveMediaSource(fromIndex, toIndex); - if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) { - Timeline castTimeline = castPlayer.getCurrentTimeline(); - int periodCount = castTimeline.getPeriodCount(); - if (periodCount <= fromIndex || periodCount <= toIndex) { - return false; - } - int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id; - castPlayer.moveItem(elementId, toIndex); - } - - mediaQueue.add(toIndex, mediaQueue.remove(fromIndex)); - - // Index update. - if (fromIndex == currentItemIndex) { - maybeSetCurrentItemAndNotify(toIndex); - } else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) { - maybeSetCurrentItemAndNotify(currentItemIndex - 1); - } else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) { - maybeSetCurrentItemAndNotify(currentItemIndex + 1); - } - - return true; - } - - /** - * Dispatches a given {@link KeyEvent} to the corresponding view of the current player. - * - * @param event The {@link KeyEvent}. - * @return Whether the event was handled by the target view. - */ - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - if (currentPlayer == exoPlayer) { - return localPlayerView.dispatchKeyEvent(event); - } else /* currentPlayer == castPlayer */ { - return castControlView.dispatchKeyEvent(event); - } - } - - /** Releases the manager and the players that it holds. */ - @Override - public void release() { - currentItemIndex = C.INDEX_UNSET; - mediaQueue.clear(); - concatenatingMediaSource.clear(); - castPlayer.setSessionAvailabilityListener(null); - castPlayer.release(); - localPlayerView.setPlayer(null); - exoPlayer.release(); - } - - // Player.EventListener implementation. - - @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - updateCurrentItemIndex(); - } - - @Override - public void onPositionDiscontinuity(@DiscontinuityReason int reason) { - updateCurrentItemIndex(); - } - - @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { - updateCurrentItemIndex(); - } - - // CastPlayer.SessionAvailabilityListener implementation. - - @Override - public void onCastSessionAvailable() { - setCurrentPlayer(castPlayer); - } - - @Override - public void onCastSessionUnavailable() { - setCurrentPlayer(exoPlayer); - } - - // Internal methods. - - private void updateCurrentItemIndex() { - int playbackState = currentPlayer.getPlaybackState(); - maybeSetCurrentItemAndNotify( - playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED - ? currentPlayer.getCurrentWindowIndex() - : C.INDEX_UNSET); - } - - private void setCurrentPlayer(Player currentPlayer) { - if (this.currentPlayer == currentPlayer) { - return; - } - - // View management. - if (currentPlayer == exoPlayer) { - localPlayerView.setVisibility(View.VISIBLE); - castControlView.hide(); - } else /* currentPlayer == castPlayer */ { - localPlayerView.setVisibility(View.GONE); - castControlView.show(); - } - - // Player state management. - long playbackPositionMs = C.TIME_UNSET; - int windowIndex = C.INDEX_UNSET; - boolean playWhenReady = false; - if (this.currentPlayer != null) { - int playbackState = this.currentPlayer.getPlaybackState(); - if (playbackState != Player.STATE_ENDED) { - playbackPositionMs = this.currentPlayer.getCurrentPosition(); - playWhenReady = this.currentPlayer.getPlayWhenReady(); - windowIndex = this.currentPlayer.getCurrentWindowIndex(); - if (windowIndex != currentItemIndex) { - playbackPositionMs = C.TIME_UNSET; - windowIndex = currentItemIndex; - } - } - this.currentPlayer.stop(true); - } else { - // This is the initial setup. No need to save any state. - } - - this.currentPlayer = currentPlayer; - - // Media queue management. - if (currentPlayer == exoPlayer) { - exoPlayer.prepare(concatenatingMediaSource); - } - - // Playback transition. - if (windowIndex != C.INDEX_UNSET) { - setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); - } - } - - /** - * Starts playback of the item at the given position. - * - * @param itemIndex The index of the item to play. - * @param positionMs The position at which playback should start. - * @param playWhenReady Whether the player should proceed when ready to do so. - */ - private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { - maybeSetCurrentItemAndNotify(itemIndex); - if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) { - MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; - for (int i = 0; i < items.length; i++) { - items[i] = buildMediaQueueItem(mediaQueue.get(i)); - } - castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); - } else { - currentPlayer.seekTo(itemIndex, positionMs); - currentPlayer.setPlayWhenReady(playWhenReady); - } - } - - private void maybeSetCurrentItemAndNotify(int currentItemIndex) { - if (this.currentItemIndex != currentItemIndex) { - int oldIndex = this.currentItemIndex; - this.currentItemIndex = currentItemIndex; - listener.onQueuePositionChanged(oldIndex, currentItemIndex); - } - } - - private static MediaSource buildMediaSource(MediaItem item) { - Uri uri = item.media.uri; - switch (item.mimeType) { - case DemoUtil.MIME_TYPE_SS: - return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_DASH: - return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_HLS: - return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_VIDEO_MP4: - return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - default: - { - throw new IllegalStateException("Unsupported type: " + item.mimeType); - } - } - } - - private static MediaQueueItem buildMediaQueueItem(MediaItem item) { - MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); - movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title); - MediaInfo.Builder mediaInfoBuilder = - new MediaInfo.Builder(item.media.uri.toString()) - .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setContentType(item.mimeType) - .setMetadata(movieMetadata); - if (!item.drmSchemes.isEmpty()) { - MediaItem.DrmScheme scheme = item.drmSchemes.get(0); - try { - // This configuration is only intended for testing and should *not* be used in production - // environments. See comment in the Cast Demo app's options provider. - JSONObject drmConfiguration = getDrmConfigurationJson(scheme); - if (drmConfiguration != null) { - mediaInfoBuilder.setCustomData(drmConfiguration); - } - } catch (JSONException e) { - throw new RuntimeException(e); - } - } - return new MediaQueueItem.Builder(mediaInfoBuilder.build()).build(); - } - - @Nullable - private static JSONObject getDrmConfigurationJson(MediaItem.DrmScheme scheme) - throws JSONException { - String drmScheme; - if (C.WIDEVINE_UUID.equals(scheme.uuid)) { - drmScheme = "widevine"; - } else if (C.PLAYREADY_UUID.equals(scheme.uuid)) { - drmScheme = "playready"; - } else { - return null; - } - MediaItem.UriBundle licenseServer = Assertions.checkNotNull(scheme.licenseServer); - JSONObject exoplayerConfig = - new JSONObject().put("withCredentials", false).put("protectionSystem", drmScheme); - if (!licenseServer.uri.equals(Uri.EMPTY)) { - exoplayerConfig.put("licenseUrl", licenseServer.uri.toString()); - } - if (!licenseServer.requestHeaders.isEmpty()) { - exoplayerConfig.put("headers", new JSONObject(licenseServer.requestHeaders)); - } - return new JSONObject().put("exoPlayerConfig", exoplayerConfig); - } -} diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index 9599da15cb..2d5a5f0ccf 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -98,6 +98,11 @@ import java.util.UUID; "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", "Clear DASH: Tears", MIME_TYPE_DASH)); + samples.add( + new Sample( + "https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8", + "Clear HLS: Angel one", + MIME_TYPE_HLS)); samples.add( new Sample( "https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4)); diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/ExoCastPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/ExoCastPlayerManager.java deleted file mode 100644 index e8ad2c1a0d..0000000000 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/ExoCastPlayerManager.java +++ /dev/null @@ -1,421 +0,0 @@ -/* - * 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.castdemo; - -import android.content.Context; -import android.net.Uri; -import androidx.annotation.Nullable; -import android.view.KeyEvent; -import android.view.View; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Player.DiscontinuityReason; -import com.google.android.exoplayer2.Player.EventListener; -import com.google.android.exoplayer2.Player.TimelineChangeReason; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.ext.cast.DefaultCastSessionManager; -import com.google.android.exoplayer2.ext.cast.ExoCastPlayer; -import com.google.android.exoplayer2.ext.cast.MediaItem; -import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; -import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.ui.PlayerControlView; -import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Log; -import com.google.android.gms.cast.framework.CastContext; -import java.util.ArrayList; - -/** Manages players and an internal media queue for the Cast demo app. */ -/* package */ class ExoCastPlayerManager - implements PlayerManager, EventListener, SessionAvailabilityListener { - - private static final String TAG = "ExoCastPlayerManager"; - private static final String USER_AGENT = "ExoCastDemoPlayer"; - private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = - new DefaultHttpDataSourceFactory(USER_AGENT); - - private final PlayerView localPlayerView; - private final PlayerControlView castControlView; - private final SimpleExoPlayer exoPlayer; - private final ExoCastPlayer exoCastPlayer; - private final ArrayList mediaQueue; - private final Listener listener; - private final ConcatenatingMediaSource concatenatingMediaSource; - - private int currentItemIndex; - private Player currentPlayer; - - /** - * Creates a new manager for {@link SimpleExoPlayer} and {@link ExoCastPlayer}. - * - * @param listener A {@link Listener}. - * @param localPlayerView The {@link PlayerView} for local playback. - * @param castControlView The {@link PlayerControlView} to control remote playback. - * @param context A {@link Context}. - * @param castContext The {@link CastContext}. - */ - public ExoCastPlayerManager( - Listener listener, - PlayerView localPlayerView, - PlayerControlView castControlView, - Context context, - CastContext castContext) { - this.listener = listener; - this.localPlayerView = localPlayerView; - this.castControlView = castControlView; - mediaQueue = new ArrayList<>(); - currentItemIndex = C.INDEX_UNSET; - concatenatingMediaSource = new ConcatenatingMediaSource(); - - DefaultTrackSelector trackSelector = new DefaultTrackSelector(); - RenderersFactory renderersFactory = new DefaultRenderersFactory(context); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector); - exoPlayer.addListener(this); - localPlayerView.setPlayer(exoPlayer); - - exoCastPlayer = - new ExoCastPlayer( - sessionManagerListener -> - new DefaultCastSessionManager(castContext, sessionManagerListener)); - exoCastPlayer.addListener(this); - exoCastPlayer.setSessionAvailabilityListener(this); - castControlView.setPlayer(exoCastPlayer); - - setCurrentPlayer(exoCastPlayer.isCastSessionAvailable() ? exoCastPlayer : exoPlayer); - } - - // Queue manipulation methods. - - /** - * Plays a specified queue item in the current player. - * - * @param itemIndex The index of the item to play. - */ - @Override - public void selectQueueItem(int itemIndex) { - setCurrentItem(itemIndex, C.TIME_UNSET, true); - } - - /** Returns the index of the currently played item. */ - @Override - public int getCurrentItemIndex() { - return currentItemIndex; - } - - /** - * Appends {@code item} to the media queue. - * - * @param item The {@link MediaItem} to append. - */ - @Override - public void addItem(MediaItem item) { - mediaQueue.add(item); - concatenatingMediaSource.addMediaSource(buildMediaSource(item)); - if (currentPlayer == exoCastPlayer) { - exoCastPlayer.addItemsToQueue(item); - } - } - - /** Returns the size of the media queue. */ - @Override - public int getMediaQueueSize() { - return mediaQueue.size(); - } - - /** - * Returns the item at the given index in the media queue. - * - * @param position The index of the item. - * @return The item at the given index in the media queue. - */ - @Override - public MediaItem getItem(int position) { - return mediaQueue.get(position); - } - - /** - * Removes the item at the given index from the media queue. - * - * @param item The item to remove. - * @return Whether the removal was successful. - */ - @Override - public boolean removeItem(MediaItem item) { - int itemIndex = mediaQueue.indexOf(item); - if (itemIndex == -1) { - // This may happen if another sender app removes items while this sender app is in "swiping - // an item" state. - return false; - } - concatenatingMediaSource.removeMediaSource(itemIndex); - mediaQueue.remove(itemIndex); - if (currentPlayer == exoCastPlayer) { - exoCastPlayer.removeItemFromQueue(itemIndex); - } - if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { - maybeSetCurrentItemAndNotify(C.INDEX_UNSET); - } else if (itemIndex < currentItemIndex) { - maybeSetCurrentItemAndNotify(currentItemIndex - 1); - } - return true; - } - - /** - * Moves an item within the queue. - * - * @param item The item to move. This method does nothing if {@code item} is not contained in the - * queue. - * @param toIndex The target index of the item in the queue. If {@code toIndex} exceeds the last - * position in the queue, {@code toIndex} is clamped to match the largest possible value. - * @return True if {@code item} was contained in the queue, and {@code toIndex} was a valid - * position. False otherwise. - */ - @Override - public boolean moveItem(MediaItem item, int toIndex) { - int indexOfItem = mediaQueue.indexOf(item); - if (indexOfItem == -1) { - // This may happen if another sender app removes items while this sender app is in "dragging - // an item" state. - return false; - } - int clampedToIndex = Math.min(toIndex, mediaQueue.size() - 1); - mediaQueue.add(clampedToIndex, mediaQueue.remove(indexOfItem)); - concatenatingMediaSource.moveMediaSource(indexOfItem, clampedToIndex); - if (currentPlayer == exoCastPlayer) { - exoCastPlayer.moveItemInQueue(indexOfItem, clampedToIndex); - } - // Index update. - maybeSetCurrentItemAndNotify(currentPlayer.getCurrentWindowIndex()); - return clampedToIndex == toIndex; - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - if (currentPlayer == exoPlayer) { - return localPlayerView.dispatchKeyEvent(event); - } else /* currentPlayer == exoCastPlayer */ { - return castControlView.dispatchKeyEvent(event); - } - } - - /** Releases the manager and the players that it holds. */ - @Override - public void release() { - currentItemIndex = C.INDEX_UNSET; - mediaQueue.clear(); - concatenatingMediaSource.clear(); - exoCastPlayer.setSessionAvailabilityListener(null); - exoCastPlayer.release(); - localPlayerView.setPlayer(null); - exoPlayer.release(); - } - - // Player.EventListener implementation. - - @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - updateCurrentItemIndex(); - } - - @Override - public void onPositionDiscontinuity(@DiscontinuityReason int reason) { - updateCurrentItemIndex(); - } - - @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { - if (currentPlayer == exoCastPlayer && reason != Player.TIMELINE_CHANGE_REASON_RESET) { - maybeUpdateLocalQueueWithRemoteQueueAndNotify(); - } - updateCurrentItemIndex(); - } - - @Override - public void onPlayerError(ExoPlaybackException error) { - Log.e(TAG, "The player encountered an error.", error); - listener.onPlayerError(); - } - - // CastPlayer.SessionAvailabilityListener implementation. - - @Override - public void onCastSessionAvailable() { - setCurrentPlayer(exoCastPlayer); - } - - @Override - public void onCastSessionUnavailable() { - setCurrentPlayer(exoPlayer); - } - - // Internal methods. - - private void maybeUpdateLocalQueueWithRemoteQueueAndNotify() { - Assertions.checkState(currentPlayer == exoCastPlayer); - boolean mediaQueuesMatch = mediaQueue.size() == exoCastPlayer.getQueueSize(); - for (int i = 0; mediaQueuesMatch && i < mediaQueue.size(); i++) { - mediaQueuesMatch = mediaQueue.get(i).uuid.equals(exoCastPlayer.getQueueItem(i).uuid); - } - if (mediaQueuesMatch) { - // The media queues match. Do nothing. - return; - } - mediaQueue.clear(); - concatenatingMediaSource.clear(); - for (int i = 0; i < exoCastPlayer.getQueueSize(); i++) { - MediaItem item = exoCastPlayer.getQueueItem(i); - mediaQueue.add(item); - concatenatingMediaSource.addMediaSource(buildMediaSource(item)); - } - listener.onQueueContentsExternallyChanged(); - } - - private void updateCurrentItemIndex() { - int playbackState = currentPlayer.getPlaybackState(); - maybeSetCurrentItemAndNotify( - playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED - ? currentPlayer.getCurrentWindowIndex() - : C.INDEX_UNSET); - } - - private void setCurrentPlayer(Player currentPlayer) { - if (this.currentPlayer == currentPlayer) { - return; - } - - // View management. - if (currentPlayer == exoPlayer) { - localPlayerView.setVisibility(View.VISIBLE); - castControlView.hide(); - } else /* currentPlayer == exoCastPlayer */ { - localPlayerView.setVisibility(View.GONE); - castControlView.show(); - } - - // Player state management. - long playbackPositionMs = C.TIME_UNSET; - int windowIndex = C.INDEX_UNSET; - boolean playWhenReady = false; - if (this.currentPlayer != null) { - int playbackState = this.currentPlayer.getPlaybackState(); - if (playbackState != Player.STATE_ENDED) { - playbackPositionMs = this.currentPlayer.getCurrentPosition(); - playWhenReady = this.currentPlayer.getPlayWhenReady(); - windowIndex = this.currentPlayer.getCurrentWindowIndex(); - if (windowIndex != currentItemIndex) { - playbackPositionMs = C.TIME_UNSET; - windowIndex = currentItemIndex; - } - } - this.currentPlayer.stop(true); - } else { - // This is the initial setup. No need to save any state. - } - - this.currentPlayer = currentPlayer; - - // Media queue management. - boolean shouldSeekInNewCurrentPlayer; - if (currentPlayer == exoPlayer) { - exoPlayer.prepare(concatenatingMediaSource); - shouldSeekInNewCurrentPlayer = true; - } else /* currentPlayer == exoCastPlayer */ { - if (exoCastPlayer.getPlaybackState() == Player.STATE_IDLE) { - exoCastPlayer.prepare(); - } - if (mediaQueue.isEmpty()) { - // Casting started with no local queue. We take the receiver app's queue as our own. - maybeUpdateLocalQueueWithRemoteQueueAndNotify(); - shouldSeekInNewCurrentPlayer = false; - } else { - // Casting started when the sender app had no queue. We just load our items into the - // receiver app's queue. If the receiver had no items in its queue, we also seek to wherever - // the sender app was playing. - int currentExoCastPlayerState = exoCastPlayer.getPlaybackState(); - shouldSeekInNewCurrentPlayer = - currentExoCastPlayerState == Player.STATE_IDLE - || currentExoCastPlayerState == Player.STATE_ENDED; - exoCastPlayer.addItemsToQueue(mediaQueue.toArray(new MediaItem[0])); - } - } - - // Playback transition. - if (shouldSeekInNewCurrentPlayer && windowIndex != C.INDEX_UNSET) { - setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); - } else if (getMediaQueueSize() > 0) { - maybeSetCurrentItemAndNotify(currentPlayer.getCurrentWindowIndex()); - } - } - - /** - * Starts playback of the item at the given position. - * - * @param itemIndex The index of the item to play. - * @param positionMs The position at which playback should start. - * @param playWhenReady Whether the player should proceed when ready to do so. - */ - private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { - maybeSetCurrentItemAndNotify(itemIndex); - currentPlayer.seekTo(itemIndex, positionMs); - if (currentPlayer.getPlaybackState() == Player.STATE_IDLE) { - if (currentPlayer == exoCastPlayer) { - exoCastPlayer.prepare(); - } else { - exoPlayer.prepare(concatenatingMediaSource); - } - } - currentPlayer.setPlayWhenReady(playWhenReady); - } - - private void maybeSetCurrentItemAndNotify(int currentItemIndex) { - if (this.currentItemIndex != currentItemIndex) { - int oldIndex = this.currentItemIndex; - this.currentItemIndex = currentItemIndex; - listener.onQueuePositionChanged(oldIndex, currentItemIndex); - } - } - - private static MediaSource buildMediaSource(MediaItem item) { - Uri uri = item.media.uri; - switch (item.mimeType) { - case DemoUtil.MIME_TYPE_SS: - return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_DASH: - return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_HLS: - return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_VIDEO_MP4: - return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - default: - { - throw new IllegalStateException("Unsupported type: " + item.mimeType); - } - } - } -} diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 5ed434eed6..c17c0a62ab 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -34,14 +34,11 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; -import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider; import com.google.android.exoplayer2.ext.cast.MediaItem; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.gms.cast.CastMediaControlIntent; import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.dynamite.DynamiteModule; @@ -120,21 +117,13 @@ public class MainActivity extends AppCompatActivity // There is no Cast context to work with. Do nothing. return; } - String applicationId = castContext.getCastOptions().getReceiverApplicationId(); - switch (applicationId) { - case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID: - case DefaultCastOptionsProvider.APP_ID_DEFAULT_RECEIVER_WITH_DRM: - playerManager = - new DefaultReceiverPlayerManager( - /* listener= */ this, - localPlayerView, - castControlView, - /* context= */ this, - castContext); - break; - default: - throw new IllegalStateException("Illegal receiver app id: " + applicationId); - } + playerManager = + new PlayerManager( + /* listener= */ this, + localPlayerView, + castControlView, + /* context= */ this, + castContext); mediaQueueList.setAdapter(mediaQueueListAdapter); } @@ -181,16 +170,6 @@ public class MainActivity extends AppCompatActivity } } - @Override - public void onQueueContentsExternallyChanged() { - mediaQueueListAdapter.notifyDataSetChanged(); - } - - @Override - public void onPlayerError() { - Toast.makeText(getApplicationContext(), R.string.player_error_msg, Toast.LENGTH_LONG).show(); - } - // Internal methods. private View buildSampleListView() { 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 c9a728b3ff..c92ebd7e94 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 @@ -15,53 +15,419 @@ */ package com.google.android.exoplayer2.castdemo; +import android.content.Context; +import android.net.Uri; +import androidx.annotation.Nullable; import android.view.KeyEvent; +import android.view.View; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.Player.TimelineChangeReason; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.ext.cast.CastPlayer; import com.google.android.exoplayer2.ext.cast.MediaItem; +import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.gms.cast.MediaInfo; +import com.google.android.gms.cast.MediaMetadata; +import com.google.android.gms.cast.MediaQueueItem; +import com.google.android.gms.cast.framework.CastContext; +import java.util.ArrayList; +import org.json.JSONException; +import org.json.JSONObject; -/** Manages the players in the Cast demo app. */ -/* package */ interface PlayerManager { +/** Manages players and an internal media queue for the demo app. */ +/* package */ class PlayerManager implements EventListener, SessionAvailabilityListener { /** Listener for events. */ interface Listener { /** Called when the currently played item of the media queue changes. */ void onQueuePositionChanged(int previousIndex, int newIndex); - - /** Called when the media queue changes due to modifications not caused by this manager. */ - void onQueueContentsExternallyChanged(); - - /** Called when an error occurs in the current player. */ - void onPlayerError(); } - /** Redirects the given {@code keyEvent} to the active player. */ - boolean dispatchKeyEvent(KeyEvent keyEvent); + private static final String USER_AGENT = "ExoCastDemoPlayer"; + private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = + new DefaultHttpDataSourceFactory(USER_AGENT); - /** Appends the given {@link MediaItem} to the media queue. */ - void addItem(MediaItem mediaItem); + private final PlayerView localPlayerView; + private final PlayerControlView castControlView; + private final SimpleExoPlayer exoPlayer; + private final CastPlayer castPlayer; + private final ArrayList mediaQueue; + private final Listener listener; + private final ConcatenatingMediaSource concatenatingMediaSource; - /** Returns the number of items in the media queue. */ - int getMediaQueueSize(); - - /** Selects the item at the given position for playback. */ - void selectQueueItem(int position); + private int currentItemIndex; + private Player currentPlayer; /** - * Returns the position of the item currently being played, or {@link C#INDEX_UNSET} if no item is - * being played. + * Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}. + * + * @param listener A {@link Listener} for queue position changes. + * @param localPlayerView The {@link PlayerView} for local playback. + * @param castControlView The {@link PlayerControlView} to control remote playback. + * @param context A {@link Context}. + * @param castContext The {@link CastContext}. */ - int getCurrentItemIndex(); + public PlayerManager( + Listener listener, + PlayerView localPlayerView, + PlayerControlView castControlView, + Context context, + CastContext castContext) { + this.listener = listener; + this.localPlayerView = localPlayerView; + this.castControlView = castControlView; + mediaQueue = new ArrayList<>(); + currentItemIndex = C.INDEX_UNSET; + concatenatingMediaSource = new ConcatenatingMediaSource(); - /** Returns the {@link MediaItem} at the given {@code position}. */ - MediaItem getItem(int position); + DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + RenderersFactory renderersFactory = new DefaultRenderersFactory(context); + exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector); + exoPlayer.addListener(this); + localPlayerView.setPlayer(exoPlayer); - /** Moves the item at position {@code from} to position {@code to}. */ - boolean moveItem(MediaItem item, int to); + castPlayer = new CastPlayer(castContext); + castPlayer.addListener(this); + castPlayer.setSessionAvailabilityListener(this); + castControlView.setPlayer(castPlayer); - /** Removes the item at position {@code index}. */ - boolean removeItem(MediaItem item); + setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); + } - /** Releases any acquired resources. */ - void release(); + // Queue manipulation methods. + + /** + * Plays a specified queue item in the current player. + * + * @param itemIndex The index of the item to play. + */ + public void selectQueueItem(int itemIndex) { + setCurrentItem(itemIndex, C.TIME_UNSET, true); + } + + /** Returns the index of the currently played item. */ + public int getCurrentItemIndex() { + return currentItemIndex; + } + + /** + * Appends {@code item} to the media queue. + * + * @param item The {@link MediaItem} to append. + */ + public void addItem(MediaItem item) { + mediaQueue.add(item); + concatenatingMediaSource.addMediaSource(buildMediaSource(item)); + if (currentPlayer == castPlayer) { + castPlayer.addItems(buildMediaQueueItem(item)); + } + } + + /** Returns the size of the media queue. */ + public int getMediaQueueSize() { + return mediaQueue.size(); + } + + /** + * Returns the item at the given index in the media queue. + * + * @param position The index of the item. + * @return The item at the given index in the media queue. + */ + public MediaItem getItem(int position) { + return mediaQueue.get(position); + } + + /** + * Removes the item at the given index from the media queue. + * + * @param item The item to remove. + * @return Whether the removal was successful. + */ + public boolean removeItem(MediaItem item) { + int itemIndex = mediaQueue.indexOf(item); + if (itemIndex == -1) { + return false; + } + concatenatingMediaSource.removeMediaSource(itemIndex); + if (currentPlayer == castPlayer) { + if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + if (castTimeline.getPeriodCount() <= itemIndex) { + return false; + } + castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id); + } + } + mediaQueue.remove(itemIndex); + if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { + maybeSetCurrentItemAndNotify(C.INDEX_UNSET); + } else if (itemIndex < currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } + return true; + } + + /** + * Moves an item within the queue. + * + * @param item The item to move. + * @param toIndex The target index of the item in the queue. + * @return Whether the item move was successful. + */ + public boolean moveItem(MediaItem item, int toIndex) { + int fromIndex = mediaQueue.indexOf(item); + if (fromIndex == -1) { + return false; + } + // Player update. + concatenatingMediaSource.moveMediaSource(fromIndex, toIndex); + if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + int periodCount = castTimeline.getPeriodCount(); + if (periodCount <= fromIndex || periodCount <= toIndex) { + return false; + } + int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id; + castPlayer.moveItem(elementId, toIndex); + } + + mediaQueue.add(toIndex, mediaQueue.remove(fromIndex)); + + // Index update. + if (fromIndex == currentItemIndex) { + maybeSetCurrentItemAndNotify(toIndex); + } else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex + 1); + } + + return true; + } + + /** + * Dispatches a given {@link KeyEvent} to the corresponding view of the current player. + * + * @param event The {@link KeyEvent}. + * @return Whether the event was handled by the target view. + */ + public boolean dispatchKeyEvent(KeyEvent event) { + if (currentPlayer == exoPlayer) { + return localPlayerView.dispatchKeyEvent(event); + } else /* currentPlayer == castPlayer */ { + return castControlView.dispatchKeyEvent(event); + } + } + + /** Releases the manager and the players that it holds. */ + public void release() { + currentItemIndex = C.INDEX_UNSET; + mediaQueue.clear(); + concatenatingMediaSource.clear(); + castPlayer.setSessionAvailabilityListener(null); + castPlayer.release(); + localPlayerView.setPlayer(null); + exoPlayer.release(); + } + + // Player.EventListener implementation. + + @Override + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + updateCurrentItemIndex(); + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + updateCurrentItemIndex(); + } + + @Override + public void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { + updateCurrentItemIndex(); + } + + // CastPlayer.SessionAvailabilityListener implementation. + + @Override + public void onCastSessionAvailable() { + setCurrentPlayer(castPlayer); + } + + @Override + public void onCastSessionUnavailable() { + setCurrentPlayer(exoPlayer); + } + + // Internal methods. + + private void updateCurrentItemIndex() { + int playbackState = currentPlayer.getPlaybackState(); + maybeSetCurrentItemAndNotify( + playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED + ? currentPlayer.getCurrentWindowIndex() + : C.INDEX_UNSET); + } + + private void setCurrentPlayer(Player currentPlayer) { + if (this.currentPlayer == currentPlayer) { + return; + } + + // View management. + if (currentPlayer == exoPlayer) { + localPlayerView.setVisibility(View.VISIBLE); + castControlView.hide(); + } else /* currentPlayer == castPlayer */ { + localPlayerView.setVisibility(View.GONE); + castControlView.show(); + } + + // Player state management. + long playbackPositionMs = C.TIME_UNSET; + int windowIndex = C.INDEX_UNSET; + boolean playWhenReady = false; + + Player previousPlayer = this.currentPlayer; + if (previousPlayer != null) { + // Save state from the previous player. + int playbackState = previousPlayer.getPlaybackState(); + if (playbackState != Player.STATE_ENDED) { + playbackPositionMs = previousPlayer.getCurrentPosition(); + playWhenReady = previousPlayer.getPlayWhenReady(); + windowIndex = previousPlayer.getCurrentWindowIndex(); + if (windowIndex != currentItemIndex) { + playbackPositionMs = C.TIME_UNSET; + windowIndex = currentItemIndex; + } + } + previousPlayer.stop(true); + } + + this.currentPlayer = currentPlayer; + + // Media queue management. + if (currentPlayer == exoPlayer) { + exoPlayer.prepare(concatenatingMediaSource); + } + + // Playback transition. + if (windowIndex != C.INDEX_UNSET) { + setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); + } + } + + /** + * Starts playback of the item at the given position. + * + * @param itemIndex The index of the item to play. + * @param positionMs The position at which playback should start. + * @param playWhenReady Whether the player should proceed when ready to do so. + */ + private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { + maybeSetCurrentItemAndNotify(itemIndex); + if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) { + MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; + for (int i = 0; i < items.length; i++) { + items[i] = buildMediaQueueItem(mediaQueue.get(i)); + } + castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); + } else { + currentPlayer.seekTo(itemIndex, positionMs); + currentPlayer.setPlayWhenReady(playWhenReady); + } + } + + private void maybeSetCurrentItemAndNotify(int currentItemIndex) { + if (this.currentItemIndex != currentItemIndex) { + int oldIndex = this.currentItemIndex; + this.currentItemIndex = currentItemIndex; + listener.onQueuePositionChanged(oldIndex, currentItemIndex); + } + } + + private static MediaSource buildMediaSource(MediaItem item) { + Uri uri = item.media.uri; + switch (item.mimeType) { + case DemoUtil.MIME_TYPE_SS: + return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + case DemoUtil.MIME_TYPE_DASH: + return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + case DemoUtil.MIME_TYPE_HLS: + return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + case DemoUtil.MIME_TYPE_VIDEO_MP4: + return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + default: + throw new IllegalStateException("Unsupported type: " + item.mimeType); + } + } + + private static MediaQueueItem buildMediaQueueItem(MediaItem item) { + MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); + movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title); + MediaInfo.Builder mediaInfoBuilder = + new MediaInfo.Builder(item.media.uri.toString()) + .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) + .setContentType(item.mimeType) + .setMetadata(movieMetadata); + if (!item.drmSchemes.isEmpty()) { + MediaItem.DrmScheme scheme = item.drmSchemes.get(0); + try { + // This configuration is only intended for testing and should *not* be used in production + // environments. See comment in the Cast Demo app's options provider. + JSONObject drmConfiguration = getDrmConfigurationJson(scheme); + if (drmConfiguration != null) { + mediaInfoBuilder.setCustomData(drmConfiguration); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + return new MediaQueueItem.Builder(mediaInfoBuilder.build()).build(); + } + + @Nullable + private static JSONObject getDrmConfigurationJson(MediaItem.DrmScheme scheme) + throws JSONException { + String drmScheme; + if (C.WIDEVINE_UUID.equals(scheme.uuid)) { + drmScheme = "widevine"; + } else if (C.PLAYREADY_UUID.equals(scheme.uuid)) { + drmScheme = "playready"; + } else { + return null; + } + MediaItem.UriBundle licenseServer = Assertions.checkNotNull(scheme.licenseServer); + JSONObject exoplayerConfig = + new JSONObject().put("withCredentials", false).put("protectionSystem", drmScheme); + if (!licenseServer.uri.equals(Uri.EMPTY)) { + exoplayerConfig.put("licenseUrl", licenseServer.uri.toString()); + } + if (!licenseServer.requestHeaders.isEmpty()) { + exoplayerConfig.put("headers", new JSONObject(licenseServer.requestHeaders)); + } + return new JSONObject().put("exoPlayerConfig", exoplayerConfig); + } } diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml index 013b50a175..2f0acd4808 100644 --- a/demos/cast/src/main/res/values/strings.xml +++ b/demos/cast/src/main/res/values/strings.xml @@ -24,6 +24,4 @@ Failed to get Cast context. Try updating Google Play Services and restart the app. - Player error encountered. Select a queue item to reprepare. Check the logcat and receiver app\'s console for more info. - diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastSessionManager.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastSessionManager.java deleted file mode 100644 index 7c1f06e8d2..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastSessionManager.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.ext.cast; - -/** Handles communication with the receiver app using a cast session. */ -public interface CastSessionManager { - - /** Factory for {@link CastSessionManager} instances. */ - interface Factory { - - /** - * Creates a {@link CastSessionManager} instance with the given listener. - * - * @param listener The listener to notify on receiver app and session state updates. - * @return The created instance. - */ - CastSessionManager create(StateListener listener); - } - - /** - * Extends {@link SessionAvailabilityListener} by adding receiver app state notifications. - * - *

        Receiver app state notifications contain a sequence number that matches the sequence number - * of the last {@link ExoCastMessage} sent (using {@link #send(ExoCastMessage)}) by this session - * manager and processed by the receiver app. Sequence numbers are non-negative numbers. - */ - interface StateListener extends SessionAvailabilityListener { - - /** - * Called when a status update is received from the Cast Receiver app. - * - * @param stateUpdate A {@link ReceiverAppStateUpdate} containing the fields included in the - * message. - */ - void onStateUpdateFromReceiverApp(ReceiverAppStateUpdate stateUpdate); - } - - /** - * Special constant representing an unset sequence number. It is guaranteed to be a negative - * value. - */ - long SEQUENCE_NUMBER_UNSET = Long.MIN_VALUE; - - /** - * Connects the session manager to the cast message bus and starts listening for session - * availability changes. Also announces that this sender app is connected to the message bus. - */ - void start(); - - /** Stops tracking the state of the cast session and closes any existing session. */ - void stopTrackingSession(); - - /** - * Same as {@link #stopTrackingSession()}, but also stops the receiver app if a session is - * currently available. - */ - void stopTrackingSessionAndCasting(); - - /** Whether a cast session is available. */ - boolean isCastSessionAvailable(); - - /** - * Sends an {@link ExoCastMessage} to the receiver app. - * - *

        A sequence number is assigned to every sent message. Message senders may mask the local - * state until a status update from the receiver app (see {@link StateListener}) is received with - * a greater or equal sequence number. - * - * @param message The message to send. - * @return The sequence number assigned to the message. - */ - long send(ExoCastMessage message); -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java index 5aed1373e5..8948173f60 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java @@ -44,7 +44,7 @@ public final class DefaultCastOptionsProvider implements OptionsProvider { * do not require DRM, the default receiver app should be used (see {@link * #APP_ID_DEFAULT_RECEIVER}). */ - // TODO: Add a documentation resource link for DRM support in the receiver app [Internal ref: + // TODO: Add a documentation resource link for DRM support in the receiver app [Internal ref: // b/128603245]. public static final String APP_ID_DEFAULT_RECEIVER_WITH_DRM = "A12D4273"; diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastSessionManager.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastSessionManager.java deleted file mode 100644 index c08a9bc352..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastSessionManager.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * 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.ext.cast; - -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Log; -import com.google.android.gms.cast.Cast; -import com.google.android.gms.cast.CastDevice; -import com.google.android.gms.cast.framework.CastContext; -import com.google.android.gms.cast.framework.CastSession; -import com.google.android.gms.cast.framework.SessionManager; -import com.google.android.gms.cast.framework.SessionManagerListener; -import java.io.IOException; -import org.json.JSONException; - -/** Implements {@link CastSessionManager} by using JSON message passing. */ -public class DefaultCastSessionManager implements CastSessionManager { - - private static final String TAG = "DefaultCastSessionManager"; - private static final String EXOPLAYER_CAST_NAMESPACE = "urn:x-cast:com.google.exoplayer.cast"; - - private final SessionManager sessionManager; - private final CastSessionListener castSessionListener; - private final StateListener stateListener; - private final Cast.MessageReceivedCallback messageReceivedCallback; - - private boolean started; - private long sequenceNumber; - private long expectedInitialStateUpdateSequence; - @Nullable private CastSession currentSession; - - /** - * @param context The Cast context from which the cast session is obtained. - * @param stateListener The listener to notify of state changes. - */ - public DefaultCastSessionManager(CastContext context, StateListener stateListener) { - this.stateListener = stateListener; - sessionManager = context.getSessionManager(); - currentSession = sessionManager.getCurrentCastSession(); - castSessionListener = new CastSessionListener(); - messageReceivedCallback = new CastMessageCallback(); - expectedInitialStateUpdateSequence = SEQUENCE_NUMBER_UNSET; - } - - @Override - public void start() { - started = true; - sessionManager.addSessionManagerListener(castSessionListener, CastSession.class); - currentSession = sessionManager.getCurrentCastSession(); - if (currentSession != null) { - setMessageCallbackOnSession(); - } - } - - @Override - public void stopTrackingSession() { - stop(/* stopCasting= */ false); - } - - @Override - public void stopTrackingSessionAndCasting() { - stop(/* stopCasting= */ true); - } - - @Override - public boolean isCastSessionAvailable() { - return currentSession != null && expectedInitialStateUpdateSequence == SEQUENCE_NUMBER_UNSET; - } - - @Override - public long send(ExoCastMessage message) { - if (currentSession != null) { - currentSession.sendMessage(EXOPLAYER_CAST_NAMESPACE, message.toJsonString(sequenceNumber)); - } else { - Log.w(TAG, "Tried to send a message with no established session. Method: " + message.method); - } - return sequenceNumber++; - } - - private void stop(boolean stopCasting) { - sessionManager.removeSessionManagerListener(castSessionListener, CastSession.class); - if (currentSession != null) { - sessionManager.endCurrentSession(stopCasting); - } - currentSession = null; - started = false; - } - - private void setCastSession(@Nullable CastSession session) { - Assertions.checkState(started); - boolean hadSession = currentSession != null; - currentSession = session; - if (!hadSession && session != null) { - setMessageCallbackOnSession(); - } else if (hadSession && session == null) { - stateListener.onCastSessionUnavailable(); - } - } - - private void setMessageCallbackOnSession() { - try { - Assertions.checkNotNull(currentSession) - .setMessageReceivedCallbacks(EXOPLAYER_CAST_NAMESPACE, messageReceivedCallback); - expectedInitialStateUpdateSequence = send(new ExoCastMessage.OnClientConnected()); - } catch (IOException e) { - throw new IllegalStateException(e); - } - } - - /** Listens for Cast session state changes. */ - private class CastSessionListener implements SessionManagerListener { - - @Override - public void onSessionStarting(CastSession castSession) {} - - @Override - public void onSessionStarted(CastSession castSession, String sessionId) { - setCastSession(castSession); - } - - @Override - public void onSessionStartFailed(CastSession castSession, int error) {} - - @Override - public void onSessionEnding(CastSession castSession) {} - - @Override - public void onSessionEnded(CastSession castSession, int error) { - setCastSession(null); - } - - @Override - public void onSessionResuming(CastSession castSession, String sessionId) {} - - @Override - public void onSessionResumed(CastSession castSession, boolean wasSuspended) { - setCastSession(castSession); - } - - @Override - public void onSessionResumeFailed(CastSession castSession, int error) {} - - @Override - public void onSessionSuspended(CastSession castSession, int reason) { - setCastSession(null); - } - } - - private class CastMessageCallback implements Cast.MessageReceivedCallback { - - @Override - public void onMessageReceived(CastDevice castDevice, String namespace, String message) { - if (!EXOPLAYER_CAST_NAMESPACE.equals(namespace)) { - // Non-matching namespace. Ignore. - Log.e(TAG, String.format("Unrecognized namespace: '%s'.", namespace)); - return; - } - try { - ReceiverAppStateUpdate receivedUpdate = ReceiverAppStateUpdate.fromJsonMessage(message); - if (expectedInitialStateUpdateSequence == SEQUENCE_NUMBER_UNSET - || receivedUpdate.sequenceNumber >= expectedInitialStateUpdateSequence) { - stateListener.onStateUpdateFromReceiverApp(receivedUpdate); - if (expectedInitialStateUpdateSequence != SEQUENCE_NUMBER_UNSET) { - expectedInitialStateUpdateSequence = SEQUENCE_NUMBER_UNSET; - stateListener.onCastSessionAvailable(); - } - } - } catch (JSONException e) { - Log.e(TAG, "Error while parsing state update from receiver: ", e); - } - } - } -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastConstants.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastConstants.java deleted file mode 100644 index 36173bfc5d..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastConstants.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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.ext.cast; - -/** Defines constants used by the Cast extension. */ -public final class ExoCastConstants { - - private ExoCastConstants() {} - - public static final int PROTOCOL_VERSION = 0; - - // String representations. - - public static final String STR_STATE_IDLE = "IDLE"; - public static final String STR_STATE_BUFFERING = "BUFFERING"; - public static final String STR_STATE_READY = "READY"; - public static final String STR_STATE_ENDED = "ENDED"; - - public static final String STR_REPEAT_MODE_OFF = "OFF"; - public static final String STR_REPEAT_MODE_ONE = "ONE"; - public static final String STR_REPEAT_MODE_ALL = "ALL"; - - public static final String STR_DISCONTINUITY_REASON_PERIOD_TRANSITION = "PERIOD_TRANSITION"; - public static final String STR_DISCONTINUITY_REASON_SEEK = "SEEK"; - public static final String STR_DISCONTINUITY_REASON_SEEK_ADJUSTMENT = "SEEK_ADJUSTMENT"; - public static final String STR_DISCONTINUITY_REASON_AD_INSERTION = "AD_INSERTION"; - public static final String STR_DISCONTINUITY_REASON_INTERNAL = "INTERNAL"; - - public static final String STR_SELECTION_FLAG_DEFAULT = "DEFAULT"; - public static final String STR_SELECTION_FLAG_FORCED = "FORCED"; - public static final String STR_SELECTION_FLAG_AUTOSELECT = "AUTOSELECT"; - - // Methods. - - public static final String METHOD_BASE = "player."; - - public static final String METHOD_ON_CLIENT_CONNECTED = METHOD_BASE + "onClientConnected"; - public static final String METHOD_ADD_ITEMS = METHOD_BASE + "addItems"; - public static final String METHOD_MOVE_ITEM = METHOD_BASE + "moveItem"; - public static final String METHOD_PREPARE = METHOD_BASE + "prepare"; - public static final String METHOD_REMOVE_ITEMS = METHOD_BASE + "removeItems"; - public static final String METHOD_SET_PLAY_WHEN_READY = METHOD_BASE + "setPlayWhenReady"; - public static final String METHOD_SET_REPEAT_MODE = METHOD_BASE + "setRepeatMode"; - public static final String METHOD_SET_SHUFFLE_MODE_ENABLED = - METHOD_BASE + "setShuffleModeEnabled"; - public static final String METHOD_SEEK_TO = METHOD_BASE + "seekTo"; - public static final String METHOD_SET_PLAYBACK_PARAMETERS = METHOD_BASE + "setPlaybackParameters"; - public static final String METHOD_SET_TRACK_SELECTION_PARAMETERS = - METHOD_BASE + ".setTrackSelectionParameters"; - public static final String METHOD_STOP = METHOD_BASE + "stop"; - - // JSON message keys. - - public static final String KEY_ARGS = "args"; - public static final String KEY_DEFAULT_START_POSITION_US = "defaultStartPositionUs"; - public static final String KEY_DESCRIPTION = "description"; - public static final String KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS = - "disabledTextTrackSelectionFlags"; - public static final String KEY_DISCONTINUITY_REASON = "discontinuityReason"; - public static final String KEY_DRM_SCHEMES = "drmSchemes"; - public static final String KEY_DURATION_US = "durationUs"; - public static final String KEY_END_POSITION_US = "endPositionUs"; - public static final String KEY_ERROR_MESSAGE = "error"; - public static final String KEY_ID = "id"; - public static final String KEY_INDEX = "index"; - public static final String KEY_IS_DYNAMIC = "isDynamic"; - public static final String KEY_IS_LOADING = "isLoading"; - public static final String KEY_IS_SEEKABLE = "isSeekable"; - public static final String KEY_ITEMS = "items"; - public static final String KEY_LICENSE_SERVER = "licenseServer"; - public static final String KEY_MEDIA = "media"; - public static final String KEY_MEDIA_ITEMS_INFO = "mediaItemsInfo"; - public static final String KEY_MEDIA_QUEUE = "mediaQueue"; - public static final String KEY_METHOD = "method"; - public static final String KEY_MIME_TYPE = "mimeType"; - public static final String KEY_PERIOD_ID = "periodId"; - public static final String KEY_PERIODS = "periods"; - public static final String KEY_PITCH = "pitch"; - public static final String KEY_PLAY_WHEN_READY = "playWhenReady"; - public static final String KEY_PLAYBACK_PARAMETERS = "playbackParameters"; - public static final String KEY_PLAYBACK_POSITION = "playbackPosition"; - public static final String KEY_PLAYBACK_STATE = "playbackState"; - public static final String KEY_POSITION_IN_FIRST_PERIOD_US = "positionInFirstPeriodUs"; - public static final String KEY_POSITION_MS = "positionMs"; - public static final String KEY_PREFERRED_AUDIO_LANGUAGE = "preferredAudioLanguage"; - public static final String KEY_PREFERRED_TEXT_LANGUAGE = "preferredTextLanguage"; - public static final String KEY_PROTOCOL_VERSION = "protocolVersion"; - public static final String KEY_REPEAT_MODE = "repeatMode"; - public static final String KEY_REQUEST_HEADERS = "requestHeaders"; - public static final String KEY_RESET = "reset"; - public static final String KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE = - "selectUndeterminedTextLanguage"; - public static final String KEY_SEQUENCE_NUMBER = "sequenceNumber"; - public static final String KEY_SHUFFLE_MODE_ENABLED = "shuffleModeEnabled"; - public static final String KEY_SHUFFLE_ORDER = "shuffleOrder"; - public static final String KEY_SKIP_SILENCE = "skipSilence"; - public static final String KEY_SPEED = "speed"; - public static final String KEY_START_POSITION_US = "startPositionUs"; - public static final String KEY_TITLE = "title"; - public static final String KEY_TRACK_SELECTION_PARAMETERS = "trackSelectionParameters"; - public static final String KEY_URI = "uri"; - public static final String KEY_UUID = "uuid"; - public static final String KEY_UUIDS = "uuids"; - public static final String KEY_WINDOW_DURATION_US = "windowDurationUs"; -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastMessage.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastMessage.java deleted file mode 100644 index 1529e9f5ac..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastMessage.java +++ /dev/null @@ -1,474 +0,0 @@ -/* - * 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.ext.cast; - -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ARGS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DESCRIPTION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DRM_SCHEMES; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_END_POSITION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_INDEX; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ITEMS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_LICENSE_SERVER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_METHOD; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MIME_TYPE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PROTOCOL_VERSION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REQUEST_HEADERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_RESET; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_START_POSITION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TITLE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_URI; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUIDS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_ADD_ITEMS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_MOVE_ITEM; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_ON_CLIENT_CONNECTED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_PREPARE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_REMOVE_ITEMS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SEEK_TO; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAYBACK_PARAMETERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAY_WHEN_READY; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_REPEAT_MODE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_SHUFFLE_MODE_ENABLED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_TRACK_SELECTION_PARAMETERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_STOP; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.PROTOCOL_VERSION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ALL; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ONE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_AUTOSELECT; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_DEFAULT; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_FORCED; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.ext.cast.MediaItem.UriBundle; -import com.google.android.exoplayer2.source.ShuffleOrder; -import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.UUID; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -// TODO(Internal b/118432277): Evaluate using a proto for sending to the receiver app. -/** A serializable message for operating a media player. */ -public abstract class ExoCastMessage { - - /** Notifies the receiver app of the connection of a sender app to the message bus. */ - public static final class OnClientConnected extends ExoCastMessage { - - public OnClientConnected() { - super(METHOD_ON_CLIENT_CONNECTED); - } - - @Override - protected JSONObject getArgumentsAsJsonObject() { - // No arguments needed. - return new JSONObject(); - } - } - - /** Transitions the player out of {@link Player#STATE_IDLE}. */ - public static final class Prepare extends ExoCastMessage { - - public Prepare() { - super(METHOD_PREPARE); - } - - @Override - protected JSONObject getArgumentsAsJsonObject() { - // No arguments needed. - return new JSONObject(); - } - } - - /** Transitions the player to {@link Player#STATE_IDLE} and optionally resets its state. */ - public static final class Stop extends ExoCastMessage { - - /** Whether the player state should be reset. */ - public final boolean reset; - - public Stop(boolean reset) { - super(METHOD_STOP); - this.reset = reset; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - return new JSONObject().put(KEY_RESET, reset); - } - } - - /** Adds items to a media player queue. */ - public static final class AddItems extends ExoCastMessage { - - /** - * The index at which the {@link #items} should be inserted. If {@link C#INDEX_UNSET}, the items - * are appended to the queue. - */ - public final int index; - /** The {@link MediaItem items} to add to the media queue. */ - public final List items; - /** - * The shuffle order to use for the media queue that results of adding the items to the queue. - */ - public final ShuffleOrder shuffleOrder; - - /** - * @param index See {@link #index}. - * @param items See {@link #items}. - * @param shuffleOrder See {@link #shuffleOrder}. - */ - public AddItems(int index, List items, ShuffleOrder shuffleOrder) { - super(METHOD_ADD_ITEMS); - this.index = index; - this.items = Collections.unmodifiableList(new ArrayList<>(items)); - this.shuffleOrder = shuffleOrder; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - JSONObject arguments = - new JSONObject() - .put(KEY_ITEMS, getItemsAsJsonArray()) - .put(KEY_SHUFFLE_ORDER, getShuffleOrderAsJson(shuffleOrder)); - maybePutValue(arguments, KEY_INDEX, index, C.INDEX_UNSET); - return arguments; - } - - private JSONArray getItemsAsJsonArray() throws JSONException { - JSONArray result = new JSONArray(); - for (MediaItem item : items) { - result.put(mediaItemAsJsonObject(item)); - } - return result; - } - } - - /** Moves an item in a player media queue. */ - public static final class MoveItem extends ExoCastMessage { - - /** The {@link MediaItem#uuid} of the item to move. */ - public final UUID uuid; - /** The index in the queue to which the item should be moved. */ - public final int index; - /** The shuffle order to use for the media queue that results of moving the item. */ - public ShuffleOrder shuffleOrder; - - /** - * @param uuid See {@link #uuid}. - * @param index See {@link #index}. - * @param shuffleOrder See {@link #shuffleOrder}. - */ - public MoveItem(UUID uuid, int index, ShuffleOrder shuffleOrder) { - super(METHOD_MOVE_ITEM); - this.uuid = uuid; - this.index = index; - this.shuffleOrder = shuffleOrder; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - return new JSONObject() - .put(KEY_UUID, uuid) - .put(KEY_INDEX, index) - .put(KEY_SHUFFLE_ORDER, getShuffleOrderAsJson(shuffleOrder)); - } - } - - /** Removes items from a player queue. */ - public static final class RemoveItems extends ExoCastMessage { - - /** The {@link MediaItem#uuid} of the items to remove from the queue. */ - public final List uuids; - - /** @param uuids See {@link #uuids}. */ - public RemoveItems(List uuids) { - super(METHOD_REMOVE_ITEMS); - this.uuids = Collections.unmodifiableList(new ArrayList<>(uuids)); - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - return new JSONObject().put(KEY_UUIDS, new JSONArray(uuids)); - } - } - - /** See {@link Player#setPlayWhenReady(boolean)}. */ - public static final class SetPlayWhenReady extends ExoCastMessage { - - /** The {@link Player#setPlayWhenReady(boolean) playWhenReady} value to set. */ - public final boolean playWhenReady; - - /** @param playWhenReady See {@link #playWhenReady}. */ - public SetPlayWhenReady(boolean playWhenReady) { - super(METHOD_SET_PLAY_WHEN_READY); - this.playWhenReady = playWhenReady; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - return new JSONObject().put(KEY_PLAY_WHEN_READY, playWhenReady); - } - } - - /** - * Sets the repeat mode of the media player. - * - * @see Player#setRepeatMode(int) - */ - public static final class SetRepeatMode extends ExoCastMessage { - - /** The {@link Player#setRepeatMode(int) repeatMode} to set. */ - @Player.RepeatMode public final int repeatMode; - - /** @param repeatMode See {@link #repeatMode}. */ - public SetRepeatMode(@Player.RepeatMode int repeatMode) { - super(METHOD_SET_REPEAT_MODE); - this.repeatMode = repeatMode; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - return new JSONObject().put(KEY_REPEAT_MODE, repeatModeToString(repeatMode)); - } - - private static String repeatModeToString(@Player.RepeatMode int repeatMode) { - switch (repeatMode) { - case REPEAT_MODE_OFF: - return STR_REPEAT_MODE_OFF; - case REPEAT_MODE_ONE: - return STR_REPEAT_MODE_ONE; - case REPEAT_MODE_ALL: - return STR_REPEAT_MODE_ALL; - default: - throw new AssertionError("Illegal repeat mode: " + repeatMode); - } - } - } - - /** - * Enables and disables shuffle mode in the media player. - * - * @see Player#setShuffleModeEnabled(boolean) - */ - public static final class SetShuffleModeEnabled extends ExoCastMessage { - - /** The {@link Player#setShuffleModeEnabled(boolean) shuffleModeEnabled} value to set. */ - public boolean shuffleModeEnabled; - - /** @param shuffleModeEnabled See {@link #shuffleModeEnabled}. */ - public SetShuffleModeEnabled(boolean shuffleModeEnabled) { - super(METHOD_SET_SHUFFLE_MODE_ENABLED); - this.shuffleModeEnabled = shuffleModeEnabled; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - return new JSONObject().put(KEY_SHUFFLE_MODE_ENABLED, shuffleModeEnabled); - } - } - - /** See {@link Player#seekTo(int, long)}. */ - public static final class SeekTo extends ExoCastMessage { - - /** The {@link MediaItem#uuid} of the item to seek to. */ - public final UUID uuid; - /** - * The seek position in milliseconds in the specified item. If {@link C#TIME_UNSET}, the target - * position is the item's default position. - */ - public final long positionMs; - - /** - * @param uuid See {@link #uuid}. - * @param positionMs See {@link #positionMs}. - */ - public SeekTo(UUID uuid, long positionMs) { - super(METHOD_SEEK_TO); - this.uuid = uuid; - this.positionMs = positionMs; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - JSONObject result = new JSONObject().put(KEY_UUID, uuid); - ExoCastMessage.maybePutValue(result, KEY_POSITION_MS, positionMs, C.TIME_UNSET); - return result; - } - } - - /** See {@link Player#setPlaybackParameters(PlaybackParameters)}. */ - public static final class SetPlaybackParameters extends ExoCastMessage { - - /** The {@link Player#setPlaybackParameters(PlaybackParameters) parameters} to set. */ - public final PlaybackParameters playbackParameters; - - /** @param playbackParameters See {@link #playbackParameters}. */ - public SetPlaybackParameters(PlaybackParameters playbackParameters) { - super(METHOD_SET_PLAYBACK_PARAMETERS); - this.playbackParameters = playbackParameters; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - return new JSONObject() - .put(KEY_SPEED, playbackParameters.speed) - .put(KEY_PITCH, playbackParameters.pitch) - .put(KEY_SKIP_SILENCE, playbackParameters.skipSilence); - } - } - - /** See {@link ExoCastPlayer#setTrackSelectionParameters(TrackSelectionParameters)}. */ - public static final class SetTrackSelectionParameters extends ExoCastMessage { - - /** - * The {@link ExoCastPlayer#setTrackSelectionParameters(TrackSelectionParameters) parameters} to - * set - */ - public final TrackSelectionParameters trackSelectionParameters; - - public SetTrackSelectionParameters(TrackSelectionParameters trackSelectionParameters) { - super(METHOD_SET_TRACK_SELECTION_PARAMETERS); - this.trackSelectionParameters = trackSelectionParameters; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - JSONArray disabledTextSelectionFlagsJson = new JSONArray(); - int disabledSelectionFlags = trackSelectionParameters.disabledTextTrackSelectionFlags; - if ((disabledSelectionFlags & C.SELECTION_FLAG_AUTOSELECT) != 0) { - disabledTextSelectionFlagsJson.put(STR_SELECTION_FLAG_AUTOSELECT); - } - if ((disabledSelectionFlags & C.SELECTION_FLAG_FORCED) != 0) { - disabledTextSelectionFlagsJson.put(STR_SELECTION_FLAG_FORCED); - } - if ((disabledSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0) { - disabledTextSelectionFlagsJson.put(STR_SELECTION_FLAG_DEFAULT); - } - return new JSONObject() - .put(KEY_PREFERRED_AUDIO_LANGUAGE, trackSelectionParameters.preferredAudioLanguage) - .put(KEY_PREFERRED_TEXT_LANGUAGE, trackSelectionParameters.preferredTextLanguage) - .put(KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS, disabledTextSelectionFlagsJson) - .put( - KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE, - trackSelectionParameters.selectUndeterminedTextLanguage); - } - } - - public final String method; - - /** - * Creates a message with the given method. - * - * @param method The method of the message. - */ - protected ExoCastMessage(String method) { - this.method = method; - } - - /** - * Returns a string containing a JSON representation of this message. - * - * @param sequenceNumber The sequence number to associate with this message. - * @return A string containing a JSON representation of this message. - */ - public final String toJsonString(long sequenceNumber) { - try { - JSONObject message = - new JSONObject() - .put(KEY_PROTOCOL_VERSION, PROTOCOL_VERSION) - .put(KEY_METHOD, method) - .put(KEY_SEQUENCE_NUMBER, sequenceNumber) - .put(KEY_ARGS, getArgumentsAsJsonObject()); - return message.toString(); - } catch (JSONException e) { - throw new AssertionError(e); - } - } - - /** Returns a {@link JSONObject} representation of the given item. */ - protected static JSONObject mediaItemAsJsonObject(MediaItem item) throws JSONException { - JSONObject itemAsJson = new JSONObject(); - itemAsJson.put(KEY_UUID, item.uuid); - itemAsJson.put(KEY_TITLE, item.title); - itemAsJson.put(KEY_DESCRIPTION, item.description); - itemAsJson.put(KEY_MEDIA, uriBundleAsJsonObject(item.media)); - // TODO(Internal b/118431961): Add attachment management. - - JSONArray drmSchemesAsJson = new JSONArray(); - for (MediaItem.DrmScheme drmScheme : item.drmSchemes) { - JSONObject drmSchemeAsJson = new JSONObject(); - drmSchemeAsJson.put(KEY_UUID, drmScheme.uuid); - if (drmScheme.licenseServer != null) { - drmSchemeAsJson.put(KEY_LICENSE_SERVER, uriBundleAsJsonObject(drmScheme.licenseServer)); - } - drmSchemesAsJson.put(drmSchemeAsJson); - } - itemAsJson.put(KEY_DRM_SCHEMES, drmSchemesAsJson); - maybePutValue(itemAsJson, KEY_START_POSITION_US, item.startPositionUs, C.TIME_UNSET); - maybePutValue(itemAsJson, KEY_END_POSITION_US, item.endPositionUs, C.TIME_UNSET); - itemAsJson.put(KEY_MIME_TYPE, item.mimeType); - return itemAsJson; - } - - /** Returns a {@link JSONObject JSON object} containing the arguments of the message. */ - protected abstract JSONObject getArgumentsAsJsonObject() throws JSONException; - - /** Returns a JSON representation of the given {@link UriBundle}. */ - protected static JSONObject uriBundleAsJsonObject(UriBundle uriBundle) throws JSONException { - return new JSONObject() - .put(KEY_URI, uriBundle.uri) - .put(KEY_REQUEST_HEADERS, new JSONObject(uriBundle.requestHeaders)); - } - - private static JSONArray getShuffleOrderAsJson(ShuffleOrder shuffleOrder) { - JSONArray shuffleOrderJson = new JSONArray(); - int index = shuffleOrder.getFirstIndex(); - while (index != C.INDEX_UNSET) { - shuffleOrderJson.put(index); - index = shuffleOrder.getNextIndex(index); - } - return shuffleOrderJson; - } - - private static void maybePutValue(JSONObject target, String key, long value, long unsetValue) - throws JSONException { - if (value != unsetValue) { - target.put(key, value); - } - } -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastOptionsProvider.java deleted file mode 100644 index 56b5d3cc8c..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastOptionsProvider.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.ext.cast; - -import android.content.Context; -import androidx.annotation.Nullable; -import com.google.android.gms.cast.framework.CastOptions; -import com.google.android.gms.cast.framework.OptionsProvider; -import com.google.android.gms.cast.framework.SessionProvider; -import java.util.List; - -/** Cast options provider to target ExoPlayer's custom receiver app. */ -public final class ExoCastOptionsProvider implements OptionsProvider { - - public static final String RECEIVER_ID = "365DCC88"; - - @Override - public CastOptions getCastOptions(Context context) { - return new CastOptions.Builder().setReceiverApplicationId(RECEIVER_ID).build(); - } - - @Override - @Nullable - public List getAdditionalSessionProviders(Context context) { - return null; - } -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayer.java deleted file mode 100644 index e24970ba0d..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayer.java +++ /dev/null @@ -1,958 +0,0 @@ -/* - * 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.ext.cast; - -import android.os.Looper; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.BasePlayer; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.IllegalSeekPositionException; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.ext.cast.ExoCastMessage.AddItems; -import com.google.android.exoplayer2.ext.cast.ExoCastMessage.MoveItem; -import com.google.android.exoplayer2.ext.cast.ExoCastMessage.RemoveItems; -import com.google.android.exoplayer2.ext.cast.ExoCastMessage.SetRepeatMode; -import com.google.android.exoplayer2.ext.cast.ExoCastMessage.SetShuffleModeEnabled; -import com.google.android.exoplayer2.ext.cast.ExoCastMessage.SetTrackSelectionParameters; -import com.google.android.exoplayer2.ext.cast.ExoCastTimeline.PeriodUid; -import com.google.android.exoplayer2.source.ShuffleOrder; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; -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.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.CopyOnWriteArrayList; -import org.checkerframework.checker.nullness.compatqual.NullableType; - -/** - * Plays media in a Cast receiver app that implements the ExoCast message protocol. - * - *

        The ExoCast communication protocol consists in exchanging serialized {@link ExoCastMessage - * ExoCastMessages} and {@link ReceiverAppStateUpdate receiver app state updates}. - * - *

        All methods in this class must be invoked on the main thread. Operations that change the state - * of the receiver app are masked locally as if their effect was immediate in the receiver app. - * - *

        Methods that change the state of the player must only be invoked when a session is available, - * according to {@link CastSessionManager#isCastSessionAvailable()}. - */ -public final class ExoCastPlayer extends BasePlayer { - - private static final String TAG = "ExoCastPlayer"; - - private static final int RENDERER_COUNT = 4; - private static final int RENDERER_INDEX_VIDEO = 0; - private static final int RENDERER_INDEX_AUDIO = 1; - private static final int RENDERER_INDEX_TEXT = 2; - private static final int RENDERER_INDEX_METADATA = 3; - - private final Clock clock; - private final CastSessionManager castSessionManager; - private final CopyOnWriteArrayList listeners; - private final ArrayList notificationsBatch; - private final ArrayDeque ongoingNotificationsTasks; - private final Timeline.Period scratchPeriod; - @Nullable private SessionAvailabilityListener sessionAvailabilityListener; - - // Player state. - - private final List mediaItems; - private final StateHolder currentTimeline; - private ShuffleOrder currentShuffleOrder; - - private final StateHolder playbackState; - private final StateHolder playWhenReady; - private final StateHolder repeatMode; - private final StateHolder shuffleModeEnabled; - private final StateHolder isLoading; - private final StateHolder playbackParameters; - private final StateHolder trackselectionParameters; - private final StateHolder currentTrackGroups; - private final StateHolder currentTrackSelections; - private final StateHolder<@NullableType Object> currentManifest; - private final StateHolder<@NullableType PeriodUid> currentPeriodUid; - private final StateHolder playbackPositionMs; - private final HashMap currentMediaItemInfoMap; - private long lastPlaybackPositionChangeTimeMs; - @Nullable private ExoPlaybackException playbackError; - - /** - * Creates an instance using the system clock for calculating time deltas. - * - * @param castSessionManagerFactory Factory to create the {@link CastSessionManager}. - */ - public ExoCastPlayer(CastSessionManager.Factory castSessionManagerFactory) { - this(castSessionManagerFactory, Clock.DEFAULT); - } - - /** - * Creates an instance using a custom {@link Clock} implementation. - * - * @param castSessionManagerFactory Factory to create the {@link CastSessionManager}. - * @param clock The clock to use for time delta calculations. - */ - public ExoCastPlayer(CastSessionManager.Factory castSessionManagerFactory, Clock clock) { - this.clock = clock; - castSessionManager = castSessionManagerFactory.create(new SessionManagerStateListener()); - listeners = new CopyOnWriteArrayList<>(); - notificationsBatch = new ArrayList<>(); - ongoingNotificationsTasks = new ArrayDeque<>(); - scratchPeriod = new Timeline.Period(); - mediaItems = new ArrayList<>(); - currentShuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ mediaItems.size()); - playbackState = new StateHolder<>(STATE_IDLE); - playWhenReady = new StateHolder<>(false); - repeatMode = new StateHolder<>(REPEAT_MODE_OFF); - shuffleModeEnabled = new StateHolder<>(false); - isLoading = new StateHolder<>(false); - playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT); - trackselectionParameters = new StateHolder<>(TrackSelectionParameters.DEFAULT); - currentTrackGroups = new StateHolder<>(TrackGroupArray.EMPTY); - currentTrackSelections = new StateHolder<>(new TrackSelectionArray(null, null, null, null)); - currentManifest = new StateHolder<>(null); - currentTimeline = new StateHolder<>(ExoCastTimeline.EMPTY); - playbackPositionMs = new StateHolder<>(0L); - currentPeriodUid = new StateHolder<>(null); - currentMediaItemInfoMap = new HashMap<>(); - castSessionManager.start(); - } - - /** Returns whether a Cast session is available. */ - public boolean isCastSessionAvailable() { - return castSessionManager.isCastSessionAvailable(); - } - - /** - * Sets a listener for updates on the Cast session availability. - * - * @param listener The {@link SessionAvailabilityListener}. - */ - public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) { - sessionAvailabilityListener = listener; - } - - /** - * Prepares the player for playback. - * - *

        Sends a preparation message to the receiver. If the player is in {@link #STATE_IDLE}, - * updates the timeline with the media queue contents. - */ - public void prepare() { - long sequence = castSessionManager.send(new ExoCastMessage.Prepare()); - if (playbackState.value == STATE_IDLE) { - playbackState.sequence = sequence; - setPlaybackStateInternal(mediaItems.isEmpty() ? STATE_ENDED : STATE_BUFFERING); - if (!currentTimeline.value.representsMediaQueue( - mediaItems, currentMediaItemInfoMap, currentShuffleOrder)) { - updateTimelineInternal(TIMELINE_CHANGE_REASON_PREPARED); - } - } - flushNotifications(); - } - - /** - * Returns the item at the given index. - * - * @param index The index of the item to retrieve. - * @return The item at the given index. - */ - public MediaItem getQueueItem(int index) { - return mediaItems.get(index); - } - - /** - * Equivalent to {@link #addItemsToQueue(int, MediaItem...) addItemsToQueue(C.INDEX_UNSET, - * items)}. - */ - public void addItemsToQueue(MediaItem... items) { - addItemsToQueue(C.INDEX_UNSET, items); - } - - /** - * Adds the given sequence of items to the queue at the given position, so that the first of - * {@code items} is placed at the given index. - * - *

        This method discards {@code items} with a uuid that already appears in the media queue. This - * method does nothing if {@code items} contains no new items. - * - * @param optionalIndex The index at which {@code items} will be inserted. If {@link - * C#INDEX_UNSET} is passed, the items are appended to the media queue. - * @param items The sequence of items to append. {@code items} must not contain items with - * matching uuids. - * @throws IllegalArgumentException If two or more elements in {@code items} contain matching - * uuids. - */ - public void addItemsToQueue(int optionalIndex, MediaItem... items) { - // Filter out items whose uuid already appears in the queue. - ArrayList itemsToAdd = new ArrayList<>(); - HashSet addedUuids = new HashSet<>(); - for (MediaItem item : items) { - Assertions.checkArgument( - addedUuids.add(item.uuid), "Added items must contain distinct uuids"); - if (playbackState.value == STATE_IDLE - || currentTimeline.value.getWindowIndexFromUuid(item.uuid) == C.INDEX_UNSET) { - // Prevent adding items that exist in the timeline. If the player is not yet prepared, - // ignore this check, since the timeline may not reflect the current media queue. - // Preparation will filter any duplicates. - itemsToAdd.add(item); - } - } - if (itemsToAdd.isEmpty()) { - return; - } - - int normalizedIndex; - if (optionalIndex != C.INDEX_UNSET) { - normalizedIndex = optionalIndex; - mediaItems.addAll(optionalIndex, itemsToAdd); - } else { - normalizedIndex = mediaItems.size(); - mediaItems.addAll(itemsToAdd); - } - currentShuffleOrder = currentShuffleOrder.cloneAndInsert(normalizedIndex, itemsToAdd.size()); - long sequence = - castSessionManager.send(new AddItems(optionalIndex, itemsToAdd, currentShuffleOrder)); - if (playbackState.value != STATE_IDLE) { - currentTimeline.sequence = sequence; - updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC); - } - flushNotifications(); - } - - /** - * Moves an existing item within the queue. - * - *

        Calling this method is equivalent to removing the item at position {@code indexFrom} and - * immediately inserting it at position {@code indexTo}. If the moved item is being played at the - * moment of the invocation, playback will stick with the moved item. - * - * @param index The index of the item to move. - * @param newIndex The index at which the item will be placed after this operation. - */ - public void moveItemInQueue(int index, int newIndex) { - MediaItem movedItem = mediaItems.remove(index); - mediaItems.add(newIndex, movedItem); - currentShuffleOrder = - currentShuffleOrder - .cloneAndRemove(index, index + 1) - .cloneAndInsert(newIndex, /* insertionCount= */ 1); - long sequence = - castSessionManager.send(new MoveItem(movedItem.uuid, newIndex, currentShuffleOrder)); - if (playbackState.value != STATE_IDLE) { - currentTimeline.sequence = sequence; - updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC); - } - flushNotifications(); - } - - /** - * Removes an item from the queue. - * - * @param index The index of the item to remove from the queue. - */ - public void removeItemFromQueue(int index) { - removeRangeFromQueue(index, index + 1); - } - - /** - * Removes a range of items from the queue. - * - *

        If the currently-playing item is removed, the playback position moves to the item following - * the removed range. If no item follows the removed range, the position is set to the last item - * in the queue and the player state transitions to {@link #STATE_ENDED}. Does nothing if an empty - * range ({@code from == exclusiveTo}) is passed. - * - * @param indexFrom The inclusive index at which the range to remove starts. - * @param indexExclusiveTo The exclusive index at which the range to remove ends. - */ - public void removeRangeFromQueue(int indexFrom, int indexExclusiveTo) { - UUID[] uuidsToRemove = new UUID[indexExclusiveTo - indexFrom]; - for (int i = 0; i < uuidsToRemove.length; i++) { - uuidsToRemove[i] = mediaItems.get(i + indexFrom).uuid; - } - - int windowIndexBeforeRemoval = getCurrentWindowIndex(); - boolean currentItemWasRemoved = - windowIndexBeforeRemoval >= indexFrom && windowIndexBeforeRemoval < indexExclusiveTo; - boolean shouldTransitionToEnded = - currentItemWasRemoved && indexExclusiveTo == mediaItems.size(); - - Util.removeRange(mediaItems, indexFrom, indexExclusiveTo); - long sequence = castSessionManager.send(new RemoveItems(Arrays.asList(uuidsToRemove))); - currentShuffleOrder = currentShuffleOrder.cloneAndRemove(indexFrom, indexExclusiveTo); - - if (playbackState.value != STATE_IDLE) { - currentTimeline.sequence = sequence; - updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC); - if (currentItemWasRemoved) { - int newWindowIndex = Math.max(0, indexFrom - (shouldTransitionToEnded ? 1 : 0)); - PeriodUid periodUid = - currentTimeline.value.isEmpty() - ? null - : (PeriodUid) - currentTimeline.value.getPeriodPosition( - window, - scratchPeriod, - newWindowIndex, - /* windowPositionUs= */ C.TIME_UNSET) - .first; - currentPeriodUid.sequence = sequence; - playbackPositionMs.sequence = sequence; - setPlaybackPositionInternal( - periodUid, - /* positionMs= */ C.TIME_UNSET, - /* discontinuityReason= */ DISCONTINUITY_REASON_SEEK); - } - playbackState.sequence = sequence; - setPlaybackStateInternal(shouldTransitionToEnded ? STATE_ENDED : STATE_BUFFERING); - } - flushNotifications(); - } - - /** Removes all items in the queue. */ - public void clearQueue() { - removeRangeFromQueue(0, getQueueSize()); - } - - /** Returns the number of items in this queue. */ - public int getQueueSize() { - return mediaItems.size(); - } - - // Track selection. - - /** - * Provides a set of constrains for the receiver app to execute track selection. - * - *

        {@link TrackSelectionParameters} passed to this method may be {@link - * TrackSelectionParameters#buildUpon() built upon} by this player as a result of a remote - * operation, which means {@link TrackSelectionParameters} obtained from {@link - * #getTrackSelectionParameters()} may have field differences with {@code parameters} passed to - * this method. However, only fields modified remotely will present differences. Other fields will - * remain unchanged. - */ - public void setTrackSelectionParameters(TrackSelectionParameters trackselectionParameters) { - this.trackselectionParameters.value = trackselectionParameters; - this.trackselectionParameters.sequence = - castSessionManager.send(new SetTrackSelectionParameters(trackselectionParameters)); - } - - /** - * Retrieves the current {@link TrackSelectionParameters}. See {@link - * #setTrackSelectionParameters(TrackSelectionParameters)}. - */ - public TrackSelectionParameters getTrackSelectionParameters() { - return trackselectionParameters.value; - } - - // Player Implementation. - - @Override - @Nullable - public AudioComponent getAudioComponent() { - // TODO: Implement volume controls using the audio component. - return null; - } - - @Override - @Nullable - public VideoComponent getVideoComponent() { - return null; - } - - @Override - @Nullable - public TextComponent getTextComponent() { - return null; - } - - @Override - @Nullable - public MetadataComponent getMetadataComponent() { - return null; - } - - @Override - public Looper getApplicationLooper() { - return Looper.getMainLooper(); - } - - @Override - public void addListener(EventListener listener) { - listeners.addIfAbsent(new ListenerHolder(listener)); - } - - @Override - public void removeListener(EventListener listener) { - for (ListenerHolder listenerHolder : listeners) { - if (listenerHolder.listener.equals(listener)) { - listenerHolder.release(); - listeners.remove(listenerHolder); - } - } - } - - @Override - @Player.State - public int getPlaybackState() { - return playbackState.value; - } - - @Nullable - @Override - public ExoPlaybackException getPlaybackError() { - return playbackError; - } - - @Override - public void setPlayWhenReady(boolean playWhenReady) { - this.playWhenReady.sequence = - castSessionManager.send(new ExoCastMessage.SetPlayWhenReady(playWhenReady)); - // Take a snapshot of the playback position before pausing to ensure future calculations are - // correct. - setPlaybackPositionInternal( - currentPeriodUid.value, getCurrentPosition(), /* discontinuityReason= */ null); - setPlayWhenReadyInternal(playWhenReady); - flushNotifications(); - } - - @Override - public boolean getPlayWhenReady() { - return playWhenReady.value; - } - - @Override - public void setRepeatMode(@RepeatMode int repeatMode) { - this.repeatMode.sequence = castSessionManager.send(new SetRepeatMode(repeatMode)); - setRepeatModeInternal(repeatMode); - flushNotifications(); - } - - @Override - @RepeatMode - public int getRepeatMode() { - return repeatMode.value; - } - - @Override - public void setShuffleModeEnabled(boolean shuffleModeEnabled) { - this.shuffleModeEnabled.sequence = - castSessionManager.send(new SetShuffleModeEnabled(shuffleModeEnabled)); - setShuffleModeEnabledInternal(shuffleModeEnabled); - flushNotifications(); - } - - @Override - public boolean getShuffleModeEnabled() { - return shuffleModeEnabled.value; - } - - @Override - public boolean isLoading() { - return isLoading.value; - } - - @Override - public void seekTo(int windowIndex, long positionMs) { - if (mediaItems.isEmpty()) { - // TODO: Handle seeking in empty timeline. - setPlaybackPositionInternal(/* periodUid= */ null, 0, DISCONTINUITY_REASON_SEEK); - return; - } else if (windowIndex >= mediaItems.size()) { - throw new IllegalSeekPositionException(currentTimeline.value, windowIndex, positionMs); - } - long sequence = - castSessionManager.send( - new ExoCastMessage.SeekTo(mediaItems.get(windowIndex).uuid, positionMs)); - - currentPeriodUid.sequence = sequence; - playbackPositionMs.sequence = sequence; - - PeriodUid periodUid = - (PeriodUid) - currentTimeline.value.getPeriodPosition( - window, scratchPeriod, windowIndex, C.msToUs(positionMs)) - .first; - setPlaybackPositionInternal(periodUid, positionMs, DISCONTINUITY_REASON_SEEK); - if (playbackState.value != STATE_IDLE) { - playbackState.sequence = sequence; - setPlaybackStateInternal(STATE_BUFFERING); - } - flushNotifications(); - } - - @Override - public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { - playbackParameters = - playbackParameters != null ? playbackParameters : PlaybackParameters.DEFAULT; - this.playbackParameters.value = playbackParameters; - this.playbackParameters.sequence = - castSessionManager.send(new ExoCastMessage.SetPlaybackParameters(playbackParameters)); - this.playbackParameters.value = playbackParameters; - // Note: This method, unlike others, does not immediately notify the change. See the Player - // interface for more information. - } - - @Override - public PlaybackParameters getPlaybackParameters() { - return playbackParameters.value; - } - - @Override - public void stop(boolean reset) { - long sequence = castSessionManager.send(new ExoCastMessage.Stop(reset)); - playbackState.sequence = sequence; - setPlaybackStateInternal(STATE_IDLE); - if (reset) { - currentTimeline.sequence = sequence; - mediaItems.clear(); - currentShuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length =*/ 0); - setPlaybackPositionInternal( - /* periodUid= */ null, /* positionMs= */ 0, DISCONTINUITY_REASON_INTERNAL); - updateTimelineInternal(TIMELINE_CHANGE_REASON_RESET); - } - flushNotifications(); - } - - @Override - public void release() { - setSessionAvailabilityListener(null); - castSessionManager.stopTrackingSession(); - flushNotifications(); - } - - @Override - public int getRendererCount() { - return RENDERER_COUNT; - } - - @Override - public int getRendererType(int index) { - switch (index) { - case RENDERER_INDEX_VIDEO: - return C.TRACK_TYPE_VIDEO; - case RENDERER_INDEX_AUDIO: - return C.TRACK_TYPE_AUDIO; - case RENDERER_INDEX_TEXT: - return C.TRACK_TYPE_TEXT; - case RENDERER_INDEX_METADATA: - return C.TRACK_TYPE_METADATA; - default: - throw new IndexOutOfBoundsException(); - } - } - - @Override - public TrackGroupArray getCurrentTrackGroups() { - // TODO (Internal b/62080507): Implement using track information from currentMediaItemInfoMap. - return currentTrackGroups.value; - } - - @Override - public TrackSelectionArray getCurrentTrackSelections() { - // TODO (Internal b/62080507): Implement using track information from currentMediaItemInfoMap. - return currentTrackSelections.value; - } - - @Override - @Nullable - public Object getCurrentManifest() { - // TODO (Internal b/62080507): Implement using track information from currentMediaItemInfoMap. - return currentManifest.value; - } - - @Override - public Timeline getCurrentTimeline() { - return currentTimeline.value; - } - - @Override - public int getCurrentPeriodIndex() { - int periodIndex = - currentPeriodUid.value == null - ? C.INDEX_UNSET - : currentTimeline.value.getIndexOfPeriod(currentPeriodUid.value); - return periodIndex != C.INDEX_UNSET ? periodIndex : 0; - } - - @Override - public int getCurrentWindowIndex() { - int windowIndex = - currentPeriodUid.value == null - ? C.INDEX_UNSET - : currentTimeline.value.getWindowIndexContainingPeriod(currentPeriodUid.value); - return windowIndex != C.INDEX_UNSET ? windowIndex : 0; - } - - @Override - public long getDuration() { - return getContentDuration(); - } - - @Override - public long getCurrentPosition() { - return playbackPositionMs.value - + (getPlaybackState() == STATE_READY && getPlayWhenReady() - ? projectPlaybackTimeElapsedMs() - : 0L); - } - - @Override - public long getBufferedPosition() { - return getCurrentPosition(); - } - - @Override - public long getTotalBufferedDuration() { - return 0; - } - - @Override - public boolean isPlayingAd() { - // TODO (Internal b/119293631): Add support for ads. - return false; - } - - @Override - public int getCurrentAdGroupIndex() { - return C.INDEX_UNSET; - } - - @Override - public int getCurrentAdIndexInAdGroup() { - return C.INDEX_UNSET; - } - - @Override - public long getContentPosition() { - return getCurrentPosition(); - } - - @Override - public long getContentBufferedPosition() { - return getCurrentPosition(); - } - - // Local state modifications. - - private void setPlayWhenReadyInternal(boolean playWhenReady) { - if (this.playWhenReady.value != playWhenReady) { - this.playWhenReady.value = playWhenReady; - notificationsBatch.add( - new ListenerNotificationTask( - listener -> listener.onPlayerStateChanged(playWhenReady, playbackState.value))); - } - } - - private void setPlaybackStateInternal(int playbackState) { - if (this.playbackState.value != playbackState) { - if (this.playbackState.value == STATE_IDLE) { - // We are transitioning out of STATE_IDLE. We clear any errors. - setPlaybackErrorInternal(null); - } - this.playbackState.value = playbackState; - notificationsBatch.add( - new ListenerNotificationTask( - listener -> listener.onPlayerStateChanged(playWhenReady.value, playbackState))); - } - } - - private void setRepeatModeInternal(int repeatMode) { - if (this.repeatMode.value != repeatMode) { - this.repeatMode.value = repeatMode; - notificationsBatch.add( - new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(repeatMode))); - } - } - - private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled) { - if (this.shuffleModeEnabled.value != shuffleModeEnabled) { - this.shuffleModeEnabled.value = shuffleModeEnabled; - notificationsBatch.add( - new ListenerNotificationTask( - listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled))); - } - } - - private void setIsLoadingInternal(boolean isLoading) { - if (this.isLoading.value != isLoading) { - this.isLoading.value = isLoading; - notificationsBatch.add( - new ListenerNotificationTask(listener -> listener.onLoadingChanged(isLoading))); - } - } - - private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) { - if (!this.playbackParameters.value.equals(playbackParameters)) { - this.playbackParameters.value = playbackParameters; - notificationsBatch.add( - new ListenerNotificationTask( - listener -> listener.onPlaybackParametersChanged(playbackParameters))); - } - } - - private void setPlaybackErrorInternal(@Nullable String errorMessage) { - if (errorMessage != null) { - playbackError = ExoPlaybackException.createForRemote(errorMessage); - notificationsBatch.add( - new ListenerNotificationTask( - listener -> listener.onPlayerError(Assertions.checkNotNull(playbackError)))); - } else { - playbackError = null; - } - } - - private void setPlaybackPositionInternal( - @Nullable PeriodUid periodUid, long positionMs, @Nullable Integer discontinuityReason) { - currentPeriodUid.value = periodUid; - if (periodUid == null) { - positionMs = 0L; - } else if (positionMs == C.TIME_UNSET) { - int windowIndex = currentTimeline.value.getWindowIndexContainingPeriod(periodUid); - if (windowIndex == C.INDEX_UNSET) { - positionMs = 0; - } else { - positionMs = - C.usToMs( - currentTimeline.value.getWindow(windowIndex, window, /* setTag= */ false) - .defaultPositionUs); - } - } - playbackPositionMs.value = positionMs; - lastPlaybackPositionChangeTimeMs = clock.elapsedRealtime(); - if (discontinuityReason != null) { - notificationsBatch.add( - new ListenerNotificationTask( - listener -> listener.onPositionDiscontinuity(discontinuityReason))); - } - } - - // Internal methods. - - private void updateTimelineInternal(@TimelineChangeReason int changeReason) { - currentTimeline.value = - ExoCastTimeline.createTimelineFor(mediaItems, currentMediaItemInfoMap, currentShuffleOrder); - removeStaleMediaItemInfo(); - notificationsBatch.add( - new ListenerNotificationTask( - listener -> - listener.onTimelineChanged( - currentTimeline.value, /* manifest= */ null, changeReason))); - } - - private long projectPlaybackTimeElapsedMs() { - return (long) - ((clock.elapsedRealtime() - lastPlaybackPositionChangeTimeMs) - * playbackParameters.value.speed); - } - - private void flushNotifications() { - boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty(); - ongoingNotificationsTasks.addAll(notificationsBatch); - notificationsBatch.clear(); - if (recursiveNotification) { - // This will be handled once the current notification task is finished. - return; - } - while (!ongoingNotificationsTasks.isEmpty()) { - ongoingNotificationsTasks.peekFirst().execute(); - ongoingNotificationsTasks.removeFirst(); - } - } - - /** - * Updates the current media item information by including any extra entries received from the - * receiver app. - * - * @param mediaItemsInformation A map of media item information received from the receiver app. - */ - private void updateMediaItemsInfo(Map mediaItemsInformation) { - for (Map.Entry entry : mediaItemsInformation.entrySet()) { - MediaItemInfo currentInfoForEntry = currentMediaItemInfoMap.get(entry.getKey()); - boolean shouldPutEntry = - currentInfoForEntry == null || !currentInfoForEntry.equals(entry.getValue()); - if (shouldPutEntry) { - currentMediaItemInfoMap.put(entry.getKey(), entry.getValue()); - } - } - } - - /** - * Removes stale media info entries. An entry is considered stale when the corresponding media - * item is not present in the current media queue. - */ - private void removeStaleMediaItemInfo() { - for (Iterator iterator = currentMediaItemInfoMap.keySet().iterator(); - iterator.hasNext(); ) { - UUID uuid = iterator.next(); - if (currentTimeline.value.getWindowIndexFromUuid(uuid) == C.INDEX_UNSET) { - iterator.remove(); - } - } - } - - // Internal classes. - - private class SessionManagerStateListener implements CastSessionManager.StateListener { - - @Override - public void onCastSessionAvailable() { - if (sessionAvailabilityListener != null) { - sessionAvailabilityListener.onCastSessionAvailable(); - } - } - - @Override - public void onCastSessionUnavailable() { - if (sessionAvailabilityListener != null) { - sessionAvailabilityListener.onCastSessionUnavailable(); - } - } - - @Override - public void onStateUpdateFromReceiverApp(ReceiverAppStateUpdate stateUpdate) { - long sequence = stateUpdate.sequenceNumber; - - if (stateUpdate.errorMessage != null) { - setPlaybackErrorInternal(stateUpdate.errorMessage); - } - - if (sequence >= playbackState.sequence && stateUpdate.playbackState != null) { - setPlaybackStateInternal(stateUpdate.playbackState); - } - - if (sequence >= currentTimeline.sequence) { - if (stateUpdate.items != null) { - mediaItems.clear(); - mediaItems.addAll(stateUpdate.items); - } - - currentShuffleOrder = - stateUpdate.shuffleOrder != null - ? new ShuffleOrder.DefaultShuffleOrder( - Util.toArray(stateUpdate.shuffleOrder), clock.elapsedRealtime()) - : currentShuffleOrder; - updateMediaItemsInfo(stateUpdate.mediaItemsInformation); - - if (playbackState.value != STATE_IDLE - && !currentTimeline.value.representsMediaQueue( - mediaItems, currentMediaItemInfoMap, currentShuffleOrder)) { - updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC); - } - } - - if (sequence >= currentPeriodUid.sequence - && stateUpdate.currentPlayingItemUuid != null - && stateUpdate.currentPlaybackPositionMs != null) { - PeriodUid periodUid; - if (stateUpdate.currentPlayingPeriodId == null) { - int windowIndex = - currentTimeline.value.getWindowIndexFromUuid(stateUpdate.currentPlayingItemUuid); - periodUid = - (PeriodUid) - currentTimeline.value.getPeriodPosition( - window, - scratchPeriod, - windowIndex, - C.msToUs(stateUpdate.currentPlaybackPositionMs)) - .first; - } else { - periodUid = - ExoCastTimeline.createPeriodUid( - stateUpdate.currentPlayingItemUuid, stateUpdate.currentPlayingPeriodId); - } - setPlaybackPositionInternal( - periodUid, stateUpdate.currentPlaybackPositionMs, stateUpdate.discontinuityReason); - } - - if (sequence >= isLoading.sequence && stateUpdate.isLoading != null) { - setIsLoadingInternal(stateUpdate.isLoading); - } - - if (sequence >= playWhenReady.sequence && stateUpdate.playWhenReady != null) { - setPlayWhenReadyInternal(stateUpdate.playWhenReady); - } - - if (sequence >= shuffleModeEnabled.sequence && stateUpdate.shuffleModeEnabled != null) { - setShuffleModeEnabledInternal(stateUpdate.shuffleModeEnabled); - } - - if (sequence >= repeatMode.sequence && stateUpdate.repeatMode != null) { - setRepeatModeInternal(stateUpdate.repeatMode); - } - - if (sequence >= playbackParameters.sequence && stateUpdate.playbackParameters != null) { - setPlaybackParametersInternal(stateUpdate.playbackParameters); - } - - TrackSelectionParameters parameters = stateUpdate.trackSelectionParameters; - if (sequence >= trackselectionParameters.sequence && parameters != null) { - trackselectionParameters.value = - trackselectionParameters - .value - .buildUpon() - .setDisabledTextTrackSelectionFlags(parameters.disabledTextTrackSelectionFlags) - .setPreferredAudioLanguage(parameters.preferredAudioLanguage) - .setPreferredTextLanguage(parameters.preferredTextLanguage) - .setSelectUndeterminedTextLanguage(parameters.selectUndeterminedTextLanguage) - .build(); - } - - flushNotifications(); - } - } - - private static final class StateHolder { - - public T value; - public long sequence; - - public StateHolder(T initialValue) { - value = initialValue; - sequence = CastSessionManager.SEQUENCE_NUMBER_UNSET; - } - } - - private final class ListenerNotificationTask { - - private final Iterator listenersSnapshot; - private final ListenerInvocation listenerInvocation; - - private ListenerNotificationTask(ListenerInvocation listenerInvocation) { - this.listenersSnapshot = listeners.iterator(); - this.listenerInvocation = listenerInvocation; - } - - public void execute() { - while (listenersSnapshot.hasNext()) { - listenersSnapshot.next().invoke(listenerInvocation); - } - } - } -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastTimeline.java deleted file mode 100644 index 115536ac4c..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastTimeline.java +++ /dev/null @@ -1,342 +0,0 @@ -/* - * 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.ext.cast; - -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.ShuffleOrder; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -/** - * A {@link Timeline} for Cast receiver app media queues. - * - *

        Each {@link MediaItem} in the timeline is exposed as a window. Unprepared media items are - * exposed as an unset-duration {@link Window}, with a single unset-duration {@link Period}. - */ -/* package */ final class ExoCastTimeline extends Timeline { - - /** Opaque object that uniquely identifies a period across timeline changes. */ - public interface PeriodUid {} - - /** A timeline for an empty media queue. */ - public static final ExoCastTimeline EMPTY = - createTimelineFor( - Collections.emptyList(), Collections.emptyMap(), new ShuffleOrder.DefaultShuffleOrder(0)); - - /** - * Creates {@link PeriodUid} from the given arguments. - * - * @param itemUuid The UUID that identifies the item. - * @param periodId The id of the period for which the unique identifier is required. - * @return An opaque unique identifier for a period. - */ - public static PeriodUid createPeriodUid(UUID itemUuid, Object periodId) { - return new PeriodUidImpl(itemUuid, periodId); - } - - /** - * Returns a new timeline representing the given media queue information. - * - * @param mediaItems The media items conforming the timeline. - * @param mediaItemInfoMap Maps {@link MediaItem media items} in {@code mediaItems} to a {@link - * MediaItemInfo} through their {@link MediaItem#uuid}. Media items may not have a {@link - * MediaItemInfo} mapped to them. - * @param shuffleOrder The {@link ShuffleOrder} of the timeline. {@link ShuffleOrder#getLength()} - * must be equal to {@code mediaItems.size()}. - * @return A new timeline representing the given media queue information. - */ - public static ExoCastTimeline createTimelineFor( - List mediaItems, - Map mediaItemInfoMap, - ShuffleOrder shuffleOrder) { - Assertions.checkArgument(mediaItems.size() == shuffleOrder.getLength()); - int[] accumulativePeriodCount = new int[mediaItems.size()]; - int periodCount = 0; - for (int i = 0; i < accumulativePeriodCount.length; i++) { - periodCount += getInfoOrEmpty(mediaItemInfoMap, mediaItems.get(i).uuid).periods.size(); - accumulativePeriodCount[i] = periodCount; - } - HashMap uuidToIndex = new HashMap<>(); - for (int i = 0; i < mediaItems.size(); i++) { - uuidToIndex.put(mediaItems.get(i).uuid, i); - } - return new ExoCastTimeline( - Collections.unmodifiableList(new ArrayList<>(mediaItems)), - Collections.unmodifiableMap(new HashMap<>(mediaItemInfoMap)), - Collections.unmodifiableMap(new HashMap<>(uuidToIndex)), - shuffleOrder, - accumulativePeriodCount); - } - - // Timeline backing information. - private final List mediaItems; - private final Map mediaItemInfoMap; - private final ShuffleOrder shuffleOrder; - - // Precomputed for quick access. - private final Map uuidToIndex; - private final int[] accumulativePeriodCount; - - private ExoCastTimeline( - List mediaItems, - Map mediaItemInfoMap, - Map uuidToIndex, - ShuffleOrder shuffleOrder, - int[] accumulativePeriodCount) { - this.mediaItems = mediaItems; - this.mediaItemInfoMap = mediaItemInfoMap; - this.uuidToIndex = uuidToIndex; - this.shuffleOrder = shuffleOrder; - this.accumulativePeriodCount = accumulativePeriodCount; - } - - /** - * Returns whether the given media queue information would produce a timeline equivalent to this - * one. - * - * @see ExoCastTimeline#createTimelineFor(List, Map, ShuffleOrder) - */ - public boolean representsMediaQueue( - List mediaItems, - Map mediaItemInfoMap, - ShuffleOrder shuffleOrder) { - if (this.shuffleOrder.getLength() != shuffleOrder.getLength()) { - return false; - } - - int index = shuffleOrder.getFirstIndex(); - if (this.shuffleOrder.getFirstIndex() != index) { - return false; - } - while (index != C.INDEX_UNSET) { - int nextIndex = shuffleOrder.getNextIndex(index); - if (nextIndex != this.shuffleOrder.getNextIndex(index)) { - return false; - } - index = nextIndex; - } - - if (mediaItems.size() != this.mediaItems.size()) { - return false; - } - for (int i = 0; i < mediaItems.size(); i++) { - UUID uuid = mediaItems.get(i).uuid; - MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, uuid); - if (!uuid.equals(this.mediaItems.get(i).uuid) - || !mediaItemInfo.equals(getInfoOrEmpty(this.mediaItemInfoMap, uuid))) { - return false; - } - } - return true; - } - - /** - * Returns the index of the window that contains the period identified by the given {@code - * periodUid} or {@link C#INDEX_UNSET} if this timeline does not contain any period with the given - * {@code periodUid}. - */ - public int getWindowIndexContainingPeriod(PeriodUid periodUid) { - if (!(periodUid instanceof PeriodUidImpl)) { - return C.INDEX_UNSET; - } - return getWindowIndexFromUuid(((PeriodUidImpl) periodUid).itemUuid); - } - - /** - * Returns the index of the window that represents the media item with the given {@code uuid} or - * {@link C#INDEX_UNSET} if no item in this timeline has the given {@code uuid}. - */ - public int getWindowIndexFromUuid(UUID uuid) { - Integer index = uuidToIndex.get(uuid); - return index != null ? index : C.INDEX_UNSET; - } - - // Timeline implementation. - - @Override - public int getWindowCount() { - return mediaItems.size(); - } - - @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { - MediaItem mediaItem = mediaItems.get(windowIndex); - MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, mediaItem.uuid); - return window.set( - /* tag= */ setTag ? mediaItem.attachment : null, - /* presentationStartTimeMs= */ C.TIME_UNSET, - /* windowStartTimeMs= */ C.TIME_UNSET, - /* isSeekable= */ mediaItemInfo.isSeekable, - /* isDynamic= */ mediaItemInfo.isDynamic, - /* defaultPositionUs= */ mediaItemInfo.defaultStartPositionUs, - /* durationUs= */ mediaItemInfo.windowDurationUs, - /* firstPeriodIndex= */ windowIndex == 0 ? 0 : accumulativePeriodCount[windowIndex - 1], - /* lastPeriodIndex= */ accumulativePeriodCount[windowIndex] - 1, - mediaItemInfo.positionInFirstPeriodUs); - } - - @Override - public int getPeriodCount() { - return mediaItems.isEmpty() ? 0 : accumulativePeriodCount[accumulativePeriodCount.length - 1]; - } - - @Override - public Period getPeriodByUid(Object periodUidObject, Period period) { - return getPeriodInternal((PeriodUidImpl) periodUidObject, period, /* setIds= */ true); - } - - @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - return getPeriodInternal((PeriodUidImpl) getUidOfPeriod(periodIndex), period, setIds); - } - - @Override - public int getIndexOfPeriod(Object uid) { - if (!(uid instanceof PeriodUidImpl)) { - return C.INDEX_UNSET; - } - PeriodUidImpl periodUid = (PeriodUidImpl) uid; - UUID uuid = periodUid.itemUuid; - Integer itemIndex = uuidToIndex.get(uuid); - if (itemIndex == null) { - return C.INDEX_UNSET; - } - int indexOfPeriodInItem = - getInfoOrEmpty(mediaItemInfoMap, uuid).getIndexOfPeriod(periodUid.periodId); - if (indexOfPeriodInItem == C.INDEX_UNSET) { - return C.INDEX_UNSET; - } - return indexOfPeriodInItem + (itemIndex == 0 ? 0 : accumulativePeriodCount[itemIndex - 1]); - } - - @Override - public PeriodUid getUidOfPeriod(int periodIndex) { - int mediaItemIndex = getMediaItemIndexForPeriodIndex(periodIndex); - int periodIndexInMediaItem = - periodIndex - (mediaItemIndex > 0 ? accumulativePeriodCount[mediaItemIndex - 1] : 0); - UUID uuid = mediaItems.get(mediaItemIndex).uuid; - MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, uuid); - return new PeriodUidImpl(uuid, mediaItemInfo.periods.get(periodIndexInMediaItem).id); - } - - @Override - public int getFirstWindowIndex(boolean shuffleModeEnabled) { - return shuffleModeEnabled ? shuffleOrder.getFirstIndex() : 0; - } - - @Override - public int getLastWindowIndex(boolean shuffleModeEnabled) { - return shuffleModeEnabled ? shuffleOrder.getLastIndex() : mediaItems.size() - 1; - } - - @Override - public int getPreviousWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) { - if (repeatMode == Player.REPEAT_MODE_ONE) { - return windowIndex; - } else if (windowIndex == getFirstWindowIndex(shuffleModeEnabled)) { - return repeatMode == Player.REPEAT_MODE_OFF - ? C.INDEX_UNSET - : getLastWindowIndex(shuffleModeEnabled); - } else if (shuffleModeEnabled) { - return shuffleOrder.getPreviousIndex(windowIndex); - } else { - return windowIndex - 1; - } - } - - @Override - public int getNextWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) { - if (repeatMode == Player.REPEAT_MODE_ONE) { - return windowIndex; - } else if (windowIndex == getLastWindowIndex(shuffleModeEnabled)) { - return repeatMode == Player.REPEAT_MODE_OFF - ? C.INDEX_UNSET - : getFirstWindowIndex(shuffleModeEnabled); - } else if (shuffleModeEnabled) { - return shuffleOrder.getNextIndex(windowIndex); - } else { - return windowIndex + 1; - } - } - - // Internal methods. - - private Period getPeriodInternal(PeriodUidImpl uid, Period period, boolean setIds) { - UUID uuid = uid.itemUuid; - int itemIndex = Assertions.checkNotNull(uuidToIndex.get(uuid)); - MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, uuid); - MediaItemInfo.Period mediaInfoPeriod = - mediaItemInfo.periods.get(mediaItemInfo.getIndexOfPeriod(uid.periodId)); - return period.set( - setIds ? mediaInfoPeriod.id : null, - setIds ? uid : null, - /* windowIndex= */ itemIndex, - mediaInfoPeriod.durationUs, - mediaInfoPeriod.positionInWindowUs); - } - - private int getMediaItemIndexForPeriodIndex(int periodIndex) { - return Util.binarySearchCeil( - accumulativePeriodCount, periodIndex, /* inclusive= */ false, /* stayInBounds= */ false); - } - - private static MediaItemInfo getInfoOrEmpty(Map map, UUID uuid) { - MediaItemInfo info = map.get(uuid); - return info != null ? info : MediaItemInfo.EMPTY; - } - - // Internal classes. - - private static final class PeriodUidImpl implements PeriodUid { - - public final UUID itemUuid; - public final Object periodId; - - private PeriodUidImpl(UUID itemUuid, Object periodId) { - this.itemUuid = itemUuid; - this.periodId = periodId; - } - - @Override - public boolean equals(@Nullable Object other) { - if (this == other) { - return true; - } - if (other == null || getClass() != other.getClass()) { - return false; - } - PeriodUidImpl periodUid = (PeriodUidImpl) other; - return itemUuid.equals(periodUid.itemUuid) && periodId.equals(periodUid.periodId); - } - - @Override - public int hashCode() { - int result = itemUuid.hashCode(); - result = 31 * result + periodId.hashCode(); - return result; - } - } -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemInfo.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemInfo.java deleted file mode 100644 index cb5eff4f37..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemInfo.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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.ext.cast; - -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.Util; -import java.util.Collections; -import java.util.List; - -// TODO (Internal b/119293631): Add ad playback state info. -/** - * Holds dynamic information for a {@link MediaItem}. - * - *

        Holds information related to preparation for a specific {@link MediaItem}. Unprepared items - * are associated with an {@link #EMPTY} info object until prepared. - */ -public final class MediaItemInfo { - - /** Placeholder information for media items that have not yet been prepared by the player. */ - public static final MediaItemInfo EMPTY = - new MediaItemInfo( - /* windowDurationUs= */ C.TIME_UNSET, - /* defaultStartPositionUs= */ 0L, - Collections.singletonList( - new Period( - /* id= */ new Object(), - /* durationUs= */ C.TIME_UNSET, - /* positionInWindowUs= */ 0L)), - /* positionInFirstPeriodUs= */ 0L, - /* isSeekable= */ false, - /* isDynamic= */ true); - - /** Holds the information of one of the periods of a {@link MediaItem}. */ - public static final class Period { - - /** - * The id of the period. Must be unique within the {@link MediaItem} but may match with periods - * in other items. - */ - public final Object id; - /** The duration of the period in microseconds. */ - public final long durationUs; - /** The position of this period in the window in microseconds. */ - public final long positionInWindowUs; - // TODO: Add track information. - - public Period(Object id, long durationUs, long positionInWindowUs) { - this.id = id; - this.durationUs = durationUs; - this.positionInWindowUs = positionInWindowUs; - } - - @Override - public boolean equals(@Nullable Object other) { - if (this == other) { - return true; - } - if (other == null || getClass() != other.getClass()) { - return false; - } - - Period period = (Period) other; - return durationUs == period.durationUs - && positionInWindowUs == period.positionInWindowUs - && id.equals(period.id); - } - - @Override - public int hashCode() { - int result = id.hashCode(); - result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); - result = 31 * result + (int) (positionInWindowUs ^ (positionInWindowUs >>> 32)); - return result; - } - } - - /** The duration of the window in microseconds. */ - public final long windowDurationUs; - /** The default start position relative to the start of the window, in microseconds. */ - public final long defaultStartPositionUs; - /** The periods conforming the media item. */ - public final List periods; - /** The position of the window in the first period in microseconds. */ - public final long positionInFirstPeriodUs; - /** Whether it is possible to seek within the window. */ - public final boolean isSeekable; - /** Whether the window may change when the timeline is updated. */ - public final boolean isDynamic; - - public MediaItemInfo( - long windowDurationUs, - long defaultStartPositionUs, - List periods, - long positionInFirstPeriodUs, - boolean isSeekable, - boolean isDynamic) { - this.windowDurationUs = windowDurationUs; - this.defaultStartPositionUs = defaultStartPositionUs; - this.periods = Collections.unmodifiableList(periods); - this.positionInFirstPeriodUs = positionInFirstPeriodUs; - this.isSeekable = isSeekable; - this.isDynamic = isDynamic; - } - - /** - * Returns the index of the period with {@link Period#id} equal to {@code periodId}, or {@link - * C#INDEX_UNSET} if none of the periods has the given id. - */ - public int getIndexOfPeriod(Object periodId) { - for (int i = 0; i < periods.size(); i++) { - if (Util.areEqual(periods.get(i).id, periodId)) { - return i; - } - } - return C.INDEX_UNSET; - } - - @Override - public boolean equals(@Nullable Object other) { - if (this == other) { - return true; - } - if (other == null || getClass() != other.getClass()) { - return false; - } - - MediaItemInfo that = (MediaItemInfo) other; - return windowDurationUs == that.windowDurationUs - && defaultStartPositionUs == that.defaultStartPositionUs - && positionInFirstPeriodUs == that.positionInFirstPeriodUs - && isSeekable == that.isSeekable - && isDynamic == that.isDynamic - && periods.equals(that.periods); - } - - @Override - public int hashCode() { - int result = (int) (windowDurationUs ^ (windowDurationUs >>> 32)); - result = 31 * result + (int) (defaultStartPositionUs ^ (defaultStartPositionUs >>> 32)); - result = 31 * result + periods.hashCode(); - result = 31 * result + (int) (positionInFirstPeriodUs ^ (positionInFirstPeriodUs >>> 32)); - result = 31 * result + (isSeekable ? 1 : 0); - result = 31 * result + (isDynamic ? 1 : 0); - return result; - } -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemQueue.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemQueue.java deleted file mode 100644 index 184e347e1c..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemQueue.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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.ext.cast; - -/** Represents a sequence of {@link MediaItem MediaItems}. */ -public interface MediaItemQueue { - - /** - * Returns the item at the given index. - * - * @param index The index of the item to retrieve. - * @return The item at the given index. - * @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}. - */ - MediaItem get(int index); - - /** Returns the number of items in this queue. */ - int getSize(); - - /** - * Appends the given sequence of items to the queue. - * - * @param items The sequence of items to append. - */ - void add(MediaItem... items); - - /** - * Adds the given sequence of items to the queue at the given position, so that the first of - * {@code items} is placed at the given index. - * - * @param index The index at which {@code items} will be inserted. - * @param items The sequence of items to append. - * @throws IndexOutOfBoundsException If {@code index < 0 || index > getSize()}. - */ - void add(int index, MediaItem... items); - - /** - * Moves an existing item within the playlist. - * - *

        Calling this method is equivalent to removing the item at position {@code indexFrom} and - * immediately inserting it at position {@code indexTo}. If the moved item is being played at the - * moment of the invocation, playback will stick with the moved item. - * - * @param indexFrom The index of the item to move. - * @param indexTo The index at which the item will be placed after this operation. - * @throws IndexOutOfBoundsException If for either index, {@code index < 0 || index >= getSize()}. - */ - void move(int indexFrom, int indexTo); - - /** - * Removes an item from the queue. - * - * @param index The index of the item to remove from the queue. - * @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}. - */ - void remove(int index); - - /** - * Removes a range of items from the queue. - * - *

        Does nothing if an empty range ({@code from == exclusiveTo}) is passed. - * - * @param from The inclusive index at which the range to remove starts. - * @param exclusiveTo The exclusive index at which the range to remove ends. - * @throws IndexOutOfBoundsException If {@code from < 0 || exclusiveTo > getSize() || from > - * exclusiveTo}. - */ - void removeRange(int from, int exclusiveTo); - - /** Removes all items in the queue. */ - void clear(); -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java deleted file mode 100644 index c1b12428d4..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java +++ /dev/null @@ -1,633 +0,0 @@ -/* - * 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.ext.cast; - -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AD_INSERTION; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; -import static com.google.android.exoplayer2.Player.STATE_BUFFERING; -import static com.google.android.exoplayer2.Player.STATE_ENDED; -import static com.google.android.exoplayer2.Player.STATE_IDLE; -import static com.google.android.exoplayer2.Player.STATE_READY; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DEFAULT_START_POSITION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DESCRIPTION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISCONTINUITY_REASON; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DRM_SCHEMES; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DURATION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_END_POSITION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ERROR_MESSAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_DYNAMIC; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_LOADING; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_SEEKABLE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_LICENSE_SERVER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_ITEMS_INFO; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_QUEUE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MIME_TYPE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIODS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIOD_ID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_PARAMETERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_POSITION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_STATE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_IN_FIRST_PERIOD_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REQUEST_HEADERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_START_POSITION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TITLE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TRACK_SELECTION_PARAMETERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_URI; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_WINDOW_DURATION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_AD_INSERTION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_INTERNAL; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_PERIOD_TRANSITION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_SEEK; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_SEEK_ADJUSTMENT; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ALL; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ONE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_BUFFERING; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_ENDED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_IDLE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_READY; - -import android.net.Uri; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -/** Holds a playback state update from the receiver app. */ -public final class ReceiverAppStateUpdate { - - /** Builder for {@link ReceiverAppStateUpdate}. */ - public static final class Builder { - - private final long sequenceNumber; - private @MonotonicNonNull Boolean playWhenReady; - private @MonotonicNonNull Integer playbackState; - private @MonotonicNonNull List items; - private @MonotonicNonNull Integer repeatMode; - private @MonotonicNonNull Boolean shuffleModeEnabled; - private @MonotonicNonNull Boolean isLoading; - private @MonotonicNonNull PlaybackParameters playbackParameters; - private @MonotonicNonNull TrackSelectionParameters trackSelectionParameters; - private @MonotonicNonNull String errorMessage; - private @MonotonicNonNull Integer discontinuityReason; - private @MonotonicNonNull UUID currentPlayingItemUuid; - private @MonotonicNonNull String currentPlayingPeriodId; - private @MonotonicNonNull Long currentPlaybackPositionMs; - private @MonotonicNonNull List shuffleOrder; - private Map mediaItemsInformation; - - private Builder(long sequenceNumber) { - this.sequenceNumber = sequenceNumber; - mediaItemsInformation = Collections.emptyMap(); - } - - /** See {@link ReceiverAppStateUpdate#playWhenReady}. */ - public Builder setPlayWhenReady(Boolean playWhenReady) { - this.playWhenReady = playWhenReady; - return this; - } - - /** See {@link ReceiverAppStateUpdate#playbackState}. */ - public Builder setPlaybackState(Integer playbackState) { - this.playbackState = playbackState; - return this; - } - - /** See {@link ReceiverAppStateUpdate#items}. */ - public Builder setItems(List items) { - this.items = Collections.unmodifiableList(items); - return this; - } - - /** See {@link ReceiverAppStateUpdate#repeatMode}. */ - public Builder setRepeatMode(Integer repeatMode) { - this.repeatMode = repeatMode; - return this; - } - - /** See {@link ReceiverAppStateUpdate#shuffleModeEnabled}. */ - public Builder setShuffleModeEnabled(Boolean shuffleModeEnabled) { - this.shuffleModeEnabled = shuffleModeEnabled; - return this; - } - - /** See {@link ReceiverAppStateUpdate#isLoading}. */ - public Builder setIsLoading(Boolean isLoading) { - this.isLoading = isLoading; - return this; - } - - /** See {@link ReceiverAppStateUpdate#playbackParameters}. */ - public Builder setPlaybackParameters(PlaybackParameters playbackParameters) { - this.playbackParameters = playbackParameters; - return this; - } - - /** See {@link ReceiverAppStateUpdate#trackSelectionParameters} */ - public Builder setTrackSelectionParameters(TrackSelectionParameters trackSelectionParameters) { - this.trackSelectionParameters = trackSelectionParameters; - return this; - } - - /** See {@link ReceiverAppStateUpdate#errorMessage}. */ - public Builder setErrorMessage(String errorMessage) { - this.errorMessage = errorMessage; - return this; - } - - /** See {@link ReceiverAppStateUpdate#discontinuityReason}. */ - public Builder setDiscontinuityReason(Integer discontinuityReason) { - this.discontinuityReason = discontinuityReason; - return this; - } - - /** - * See {@link ReceiverAppStateUpdate#currentPlayingItemUuid} and {@link - * ReceiverAppStateUpdate#currentPlaybackPositionMs}. - */ - public Builder setPlaybackPosition( - UUID currentPlayingItemUuid, - String currentPlayingPeriodId, - Long currentPlaybackPositionMs) { - this.currentPlayingItemUuid = currentPlayingItemUuid; - this.currentPlayingPeriodId = currentPlayingPeriodId; - this.currentPlaybackPositionMs = currentPlaybackPositionMs; - return this; - } - - /** - * See {@link ReceiverAppStateUpdate#currentPlayingItemUuid} and {@link - * ReceiverAppStateUpdate#currentPlaybackPositionMs}. - */ - public Builder setMediaItemsInformation(Map mediaItemsInformation) { - this.mediaItemsInformation = Collections.unmodifiableMap(mediaItemsInformation); - return this; - } - - /** See {@link ReceiverAppStateUpdate#shuffleOrder}. */ - public Builder setShuffleOrder(List shuffleOrder) { - this.shuffleOrder = Collections.unmodifiableList(shuffleOrder); - return this; - } - - /** - * Returns a new {@link ReceiverAppStateUpdate} instance with the current values in this - * builder. - */ - public ReceiverAppStateUpdate build() { - return new ReceiverAppStateUpdate( - sequenceNumber, - playWhenReady, - playbackState, - items, - repeatMode, - shuffleModeEnabled, - isLoading, - playbackParameters, - trackSelectionParameters, - errorMessage, - discontinuityReason, - currentPlayingItemUuid, - currentPlayingPeriodId, - currentPlaybackPositionMs, - mediaItemsInformation, - shuffleOrder); - } - } - - /** Returns a {@link ReceiverAppStateUpdate} builder. */ - public static Builder builder(long sequenceNumber) { - return new Builder(sequenceNumber); - } - - /** - * Creates an instance from parsing a state update received from the Receiver App. - * - * @param jsonMessage The state update encoded as a JSON string. - * @return The parsed state update. - * @throws JSONException If an error is encountered when parsing the {@code jsonMessage}. - */ - public static ReceiverAppStateUpdate fromJsonMessage(String jsonMessage) throws JSONException { - JSONObject stateAsJson = new JSONObject(jsonMessage); - Builder builder = builder(stateAsJson.getLong(KEY_SEQUENCE_NUMBER)); - - if (stateAsJson.has(KEY_PLAY_WHEN_READY)) { - builder.setPlayWhenReady(stateAsJson.getBoolean(KEY_PLAY_WHEN_READY)); - } - - if (stateAsJson.has(KEY_PLAYBACK_STATE)) { - builder.setPlaybackState( - playbackStateStringToConstant(stateAsJson.getString(KEY_PLAYBACK_STATE))); - } - - if (stateAsJson.has(KEY_MEDIA_QUEUE)) { - builder.setItems( - toMediaItemArrayList(Assertions.checkNotNull(stateAsJson.optJSONArray(KEY_MEDIA_QUEUE)))); - } - - if (stateAsJson.has(KEY_REPEAT_MODE)) { - builder.setRepeatMode(stringToRepeatMode(stateAsJson.getString(KEY_REPEAT_MODE))); - } - - if (stateAsJson.has(KEY_SHUFFLE_MODE_ENABLED)) { - builder.setShuffleModeEnabled(stateAsJson.getBoolean(KEY_SHUFFLE_MODE_ENABLED)); - } - - if (stateAsJson.has(KEY_IS_LOADING)) { - builder.setIsLoading(stateAsJson.getBoolean(KEY_IS_LOADING)); - } - - if (stateAsJson.has(KEY_PLAYBACK_PARAMETERS)) { - builder.setPlaybackParameters( - toPlaybackParameters( - Assertions.checkNotNull(stateAsJson.optJSONObject(KEY_PLAYBACK_PARAMETERS)))); - } - - if (stateAsJson.has(KEY_TRACK_SELECTION_PARAMETERS)) { - JSONObject trackSelectionParametersJson = - stateAsJson.getJSONObject(KEY_TRACK_SELECTION_PARAMETERS); - TrackSelectionParameters parameters = - TrackSelectionParameters.DEFAULT - .buildUpon() - .setPreferredTextLanguage( - trackSelectionParametersJson.getString(KEY_PREFERRED_TEXT_LANGUAGE)) - .setPreferredAudioLanguage( - trackSelectionParametersJson.getString(KEY_PREFERRED_AUDIO_LANGUAGE)) - .setSelectUndeterminedTextLanguage( - trackSelectionParametersJson.getBoolean(KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE)) - .setDisabledTextTrackSelectionFlags( - jsonArrayToSelectionFlags( - trackSelectionParametersJson.getJSONArray( - KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS))) - .build(); - builder.setTrackSelectionParameters(parameters); - } - - if (stateAsJson.has(KEY_ERROR_MESSAGE)) { - builder.setErrorMessage(stateAsJson.getString(KEY_ERROR_MESSAGE)); - } - - if (stateAsJson.has(KEY_PLAYBACK_POSITION)) { - JSONObject playbackPosition = stateAsJson.getJSONObject(KEY_PLAYBACK_POSITION); - String discontinuityReason = playbackPosition.optString(KEY_DISCONTINUITY_REASON); - if (!discontinuityReason.isEmpty()) { - builder.setDiscontinuityReason(stringToDiscontinuityReason(discontinuityReason)); - } - UUID currentPlayingItemUuid = UUID.fromString(playbackPosition.getString(KEY_UUID)); - String currentPlayingPeriodId = playbackPosition.getString(KEY_PERIOD_ID); - Long currentPlaybackPositionMs = playbackPosition.getLong(KEY_POSITION_MS); - builder.setPlaybackPosition( - currentPlayingItemUuid, currentPlayingPeriodId, currentPlaybackPositionMs); - } - - if (stateAsJson.has(KEY_MEDIA_ITEMS_INFO)) { - HashMap mediaItemInformation = new HashMap<>(); - JSONObject mediaItemsInfo = stateAsJson.getJSONObject(KEY_MEDIA_ITEMS_INFO); - for (Iterator i = mediaItemsInfo.keys(); i.hasNext(); ) { - String key = i.next(); - mediaItemInformation.put( - UUID.fromString(key), jsonToMediaitemInfo(mediaItemsInfo.getJSONObject(key))); - } - builder.setMediaItemsInformation(mediaItemInformation); - } - - if (stateAsJson.has(KEY_SHUFFLE_ORDER)) { - ArrayList shuffleOrder = new ArrayList<>(); - JSONArray shuffleOrderJson = stateAsJson.getJSONArray(KEY_SHUFFLE_ORDER); - for (int i = 0; i < shuffleOrderJson.length(); i++) { - shuffleOrder.add(shuffleOrderJson.getInt(i)); - } - builder.setShuffleOrder(shuffleOrder); - } - - return builder.build(); - } - - /** The sequence number of the status update. */ - public final long sequenceNumber; - /** Optional {@link Player#getPlayWhenReady playWhenReady} value. */ - @Nullable public final Boolean playWhenReady; - /** Optional {@link Player#getPlaybackState() playbackState}. */ - @Nullable public final Integer playbackState; - /** Optional list of media items. */ - @Nullable public final List items; - /** Optional {@link Player#getRepeatMode() repeatMode}. */ - @Nullable public final Integer repeatMode; - /** Optional {@link Player#getShuffleModeEnabled() shuffleMode}. */ - @Nullable public final Boolean shuffleModeEnabled; - /** Optional {@link Player#isLoading() isLoading} value. */ - @Nullable public final Boolean isLoading; - /** Optional {@link Player#getPlaybackParameters() playbackParameters}. */ - @Nullable public final PlaybackParameters playbackParameters; - /** Optional {@link TrackSelectionParameters}. */ - @Nullable public final TrackSelectionParameters trackSelectionParameters; - /** Optional error message string. */ - @Nullable public final String errorMessage; - /** - * Optional reason for a {@link Player.EventListener#onPositionDiscontinuity(int) discontinuity } - * in the playback position. - */ - @Nullable public final Integer discontinuityReason; - /** Optional {@link UUID} of the {@link Player#getCurrentWindowIndex() currently played item}. */ - @Nullable public final UUID currentPlayingItemUuid; - /** Optional id of the current {@link Player#getCurrentPeriodIndex() period being played}. */ - @Nullable public final String currentPlayingPeriodId; - /** Optional {@link Player#getCurrentPosition() playbackPosition} in milliseconds. */ - @Nullable public final Long currentPlaybackPositionMs; - /** Holds information about the {@link MediaItem media items} in the media queue. */ - public final Map mediaItemsInformation; - /** Holds the indices of the media queue items in shuffle order. */ - @Nullable public final List shuffleOrder; - - /** Creates an instance with the given values. */ - private ReceiverAppStateUpdate( - long sequenceNumber, - @Nullable Boolean playWhenReady, - @Nullable Integer playbackState, - @Nullable List items, - @Nullable Integer repeatMode, - @Nullable Boolean shuffleModeEnabled, - @Nullable Boolean isLoading, - @Nullable PlaybackParameters playbackParameters, - @Nullable TrackSelectionParameters trackSelectionParameters, - @Nullable String errorMessage, - @Nullable Integer discontinuityReason, - @Nullable UUID currentPlayingItemUuid, - @Nullable String currentPlayingPeriodId, - @Nullable Long currentPlaybackPositionMs, - Map mediaItemsInformation, - @Nullable List shuffleOrder) { - this.sequenceNumber = sequenceNumber; - this.playWhenReady = playWhenReady; - this.playbackState = playbackState; - this.items = items; - this.repeatMode = repeatMode; - this.shuffleModeEnabled = shuffleModeEnabled; - this.isLoading = isLoading; - this.playbackParameters = playbackParameters; - this.trackSelectionParameters = trackSelectionParameters; - this.errorMessage = errorMessage; - this.discontinuityReason = discontinuityReason; - this.currentPlayingItemUuid = currentPlayingItemUuid; - this.currentPlayingPeriodId = currentPlayingPeriodId; - this.currentPlaybackPositionMs = currentPlaybackPositionMs; - this.mediaItemsInformation = mediaItemsInformation; - this.shuffleOrder = shuffleOrder; - } - - @Override - public boolean equals(@Nullable Object other) { - if (this == other) { - return true; - } - if (other == null || getClass() != other.getClass()) { - return false; - } - ReceiverAppStateUpdate that = (ReceiverAppStateUpdate) other; - - return sequenceNumber == that.sequenceNumber - && Util.areEqual(playWhenReady, that.playWhenReady) - && Util.areEqual(playbackState, that.playbackState) - && Util.areEqual(items, that.items) - && Util.areEqual(repeatMode, that.repeatMode) - && Util.areEqual(shuffleModeEnabled, that.shuffleModeEnabled) - && Util.areEqual(isLoading, that.isLoading) - && Util.areEqual(playbackParameters, that.playbackParameters) - && Util.areEqual(trackSelectionParameters, that.trackSelectionParameters) - && Util.areEqual(errorMessage, that.errorMessage) - && Util.areEqual(discontinuityReason, that.discontinuityReason) - && Util.areEqual(currentPlayingItemUuid, that.currentPlayingItemUuid) - && Util.areEqual(currentPlayingPeriodId, that.currentPlayingPeriodId) - && Util.areEqual(currentPlaybackPositionMs, that.currentPlaybackPositionMs) - && Util.areEqual(mediaItemsInformation, that.mediaItemsInformation) - && Util.areEqual(shuffleOrder, that.shuffleOrder); - } - - @Override - public int hashCode() { - int result = (int) (sequenceNumber ^ (sequenceNumber >>> 32)); - result = 31 * result + (playWhenReady != null ? playWhenReady.hashCode() : 0); - result = 31 * result + (playbackState != null ? playbackState.hashCode() : 0); - result = 31 * result + (items != null ? items.hashCode() : 0); - result = 31 * result + (repeatMode != null ? repeatMode.hashCode() : 0); - result = 31 * result + (shuffleModeEnabled != null ? shuffleModeEnabled.hashCode() : 0); - result = 31 * result + (isLoading != null ? isLoading.hashCode() : 0); - result = 31 * result + (playbackParameters != null ? playbackParameters.hashCode() : 0); - result = - 31 * result + (trackSelectionParameters != null ? trackSelectionParameters.hashCode() : 0); - result = 31 * result + (errorMessage != null ? errorMessage.hashCode() : 0); - result = 31 * result + (discontinuityReason != null ? discontinuityReason.hashCode() : 0); - result = 31 * result + (currentPlayingItemUuid != null ? currentPlayingItemUuid.hashCode() : 0); - result = 31 * result + (currentPlayingPeriodId != null ? currentPlayingPeriodId.hashCode() : 0); - result = - 31 * result - + (currentPlaybackPositionMs != null ? currentPlaybackPositionMs.hashCode() : 0); - result = 31 * result + mediaItemsInformation.hashCode(); - result = 31 * result + (shuffleOrder != null ? shuffleOrder.hashCode() : 0); - return result; - } - - // Internal methods. - - @VisibleForTesting - /* package */ static List toMediaItemArrayList(JSONArray mediaItemsAsJson) - throws JSONException { - ArrayList mediaItems = new ArrayList<>(); - for (int i = 0; i < mediaItemsAsJson.length(); i++) { - mediaItems.add(toMediaItem(mediaItemsAsJson.getJSONObject(i))); - } - return mediaItems; - } - - private static MediaItem toMediaItem(JSONObject mediaItemAsJson) throws JSONException { - MediaItem.Builder builder = new MediaItem.Builder(); - builder.setUuid(UUID.fromString(mediaItemAsJson.getString(KEY_UUID))); - builder.setTitle(mediaItemAsJson.getString(KEY_TITLE)); - builder.setDescription(mediaItemAsJson.getString(KEY_DESCRIPTION)); - builder.setMedia(jsonToUriBundle(mediaItemAsJson.getJSONObject(KEY_MEDIA))); - // TODO(Internal b/118431961): Add attachment management. - - builder.setDrmSchemes(jsonArrayToDrmSchemes(mediaItemAsJson.getJSONArray(KEY_DRM_SCHEMES))); - if (mediaItemAsJson.has(KEY_START_POSITION_US)) { - builder.setStartPositionUs(mediaItemAsJson.getLong(KEY_START_POSITION_US)); - } - if (mediaItemAsJson.has(KEY_END_POSITION_US)) { - builder.setEndPositionUs(mediaItemAsJson.getLong(KEY_END_POSITION_US)); - } - builder.setMimeType(mediaItemAsJson.getString(KEY_MIME_TYPE)); - return builder.build(); - } - - private static PlaybackParameters toPlaybackParameters(JSONObject parameters) - throws JSONException { - float speed = (float) parameters.getDouble(KEY_SPEED); - float pitch = (float) parameters.getDouble(KEY_PITCH); - boolean skipSilence = parameters.getBoolean(KEY_SKIP_SILENCE); - return new PlaybackParameters(speed, pitch, skipSilence); - } - - private static int playbackStateStringToConstant(String string) { - switch (string) { - case STR_STATE_IDLE: - return STATE_IDLE; - case STR_STATE_BUFFERING: - return STATE_BUFFERING; - case STR_STATE_READY: - return STATE_READY; - case STR_STATE_ENDED: - return STATE_ENDED; - default: - throw new AssertionError("Unexpected state string: " + string); - } - } - - private static Integer stringToRepeatMode(String repeatModeStr) { - switch (repeatModeStr) { - case STR_REPEAT_MODE_OFF: - return REPEAT_MODE_OFF; - case STR_REPEAT_MODE_ONE: - return REPEAT_MODE_ONE; - case STR_REPEAT_MODE_ALL: - return REPEAT_MODE_ALL; - default: - throw new AssertionError("Illegal repeat mode: " + repeatModeStr); - } - } - - private static Integer stringToDiscontinuityReason(String discontinuityReasonStr) { - switch (discontinuityReasonStr) { - case STR_DISCONTINUITY_REASON_PERIOD_TRANSITION: - return DISCONTINUITY_REASON_PERIOD_TRANSITION; - case STR_DISCONTINUITY_REASON_SEEK: - return DISCONTINUITY_REASON_SEEK; - case STR_DISCONTINUITY_REASON_SEEK_ADJUSTMENT: - return DISCONTINUITY_REASON_SEEK_ADJUSTMENT; - case STR_DISCONTINUITY_REASON_AD_INSERTION: - return DISCONTINUITY_REASON_AD_INSERTION; - case STR_DISCONTINUITY_REASON_INTERNAL: - return DISCONTINUITY_REASON_INTERNAL; - default: - throw new AssertionError("Illegal discontinuity reason: " + discontinuityReasonStr); - } - } - - @C.SelectionFlags - private static int jsonArrayToSelectionFlags(JSONArray array) throws JSONException { - int result = 0; - for (int i = 0; i < array.length(); i++) { - switch (array.getString(i)) { - case ExoCastConstants.STR_SELECTION_FLAG_AUTOSELECT: - result |= C.SELECTION_FLAG_AUTOSELECT; - break; - case ExoCastConstants.STR_SELECTION_FLAG_FORCED: - result |= C.SELECTION_FLAG_FORCED; - break; - case ExoCastConstants.STR_SELECTION_FLAG_DEFAULT: - result |= C.SELECTION_FLAG_DEFAULT; - break; - default: - // Do nothing. - break; - } - } - return result; - } - - private static List jsonArrayToDrmSchemes(JSONArray drmSchemesAsJson) - throws JSONException { - ArrayList drmSchemes = new ArrayList<>(); - for (int i = 0; i < drmSchemesAsJson.length(); i++) { - JSONObject drmSchemeAsJson = drmSchemesAsJson.getJSONObject(i); - MediaItem.UriBundle uriBundle = - drmSchemeAsJson.has(KEY_LICENSE_SERVER) - ? jsonToUriBundle(drmSchemeAsJson.getJSONObject(KEY_LICENSE_SERVER)) - : null; - drmSchemes.add( - new MediaItem.DrmScheme(UUID.fromString(drmSchemeAsJson.getString(KEY_UUID)), uriBundle)); - } - return Collections.unmodifiableList(drmSchemes); - } - - private static MediaItem.UriBundle jsonToUriBundle(JSONObject json) throws JSONException { - Uri uri = Uri.parse(json.getString(KEY_URI)); - JSONObject requestHeadersAsJson = json.getJSONObject(KEY_REQUEST_HEADERS); - HashMap requestHeaders = new HashMap<>(); - for (Iterator i = requestHeadersAsJson.keys(); i.hasNext(); ) { - String key = i.next(); - requestHeaders.put(key, requestHeadersAsJson.getString(key)); - } - return new MediaItem.UriBundle(uri, requestHeaders); - } - - private static MediaItemInfo jsonToMediaitemInfo(JSONObject json) throws JSONException { - long durationUs = json.getLong(KEY_WINDOW_DURATION_US); - long defaultPositionUs = json.optLong(KEY_DEFAULT_START_POSITION_US, /* fallback= */ 0L); - JSONArray periodsJson = json.getJSONArray(KEY_PERIODS); - ArrayList periods = new ArrayList<>(); - long positionInFirstPeriodUs = json.getLong(KEY_POSITION_IN_FIRST_PERIOD_US); - - long windowPositionUs = -positionInFirstPeriodUs; - for (int i = 0; i < periodsJson.length(); i++) { - JSONObject periodJson = periodsJson.getJSONObject(i); - long periodDurationUs = periodJson.optLong(KEY_DURATION_US, C.TIME_UNSET); - periods.add( - new MediaItemInfo.Period( - periodJson.getString(KEY_ID), periodDurationUs, windowPositionUs)); - windowPositionUs += periodDurationUs; - } - boolean isDynamic = json.getBoolean(KEY_IS_DYNAMIC); - boolean isSeekable = json.getBoolean(KEY_IS_SEEKABLE); - return new MediaItemInfo( - durationUs, defaultPositionUs, periods, positionInFirstPeriodUs, isSeekable, isDynamic); - } -} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastMessageTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastMessageTest.java deleted file mode 100644 index b900a78937..0000000000 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastMessageTest.java +++ /dev/null @@ -1,436 +0,0 @@ -/* - * 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.ext.cast; - -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ARGS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DESCRIPTION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DRM_SCHEMES; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_END_POSITION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_INDEX; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ITEMS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_LICENSE_SERVER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_METHOD; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MIME_TYPE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REQUEST_HEADERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_START_POSITION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TITLE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_URI; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUIDS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_ADD_ITEMS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_MOVE_ITEM; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_REMOVE_ITEMS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SEEK_TO; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAYBACK_PARAMETERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAY_WHEN_READY; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_REPEAT_MODE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_SHUFFLE_MODE_ENABLED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_TRACK_SELECTION_PARAMETERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ALL; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ONE; -import static com.google.common.truth.Truth.assertThat; - -import android.net.Uri; -import androidx.annotation.Nullable; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.ext.cast.MediaItem.DrmScheme; -import com.google.android.exoplayer2.ext.cast.MediaItem.UriBundle; -import com.google.android.exoplayer2.source.ShuffleOrder; -import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; -import com.google.android.exoplayer2.util.MimeTypes; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit tests for {@link ExoCastMessage}. */ -@RunWith(AndroidJUnit4.class) -public class ExoCastMessageTest { - - @Test - public void addItems_withUnsetIndex_doesNotAddIndexToJson() throws JSONException { - MediaItem sampleItem = new MediaItem.Builder().build(); - ExoCastMessage message = - new ExoCastMessage.AddItems( - C.INDEX_UNSET, - Collections.singletonList(sampleItem), - new ShuffleOrder.UnshuffledShuffleOrder(1)); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - JSONArray items = arguments.getJSONArray(KEY_ITEMS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_ADD_ITEMS); - assertThat(arguments.has(KEY_INDEX)).isFalse(); - assertThat(items.length()).isEqualTo(1); - } - - @Test - public void addItems_withMultipleItems_producesExpectedJsonList() throws JSONException { - MediaItem sampleItem1 = new MediaItem.Builder().build(); - MediaItem sampleItem2 = new MediaItem.Builder().build(); - ExoCastMessage message = - new ExoCastMessage.AddItems( - 1, Arrays.asList(sampleItem2, sampleItem1), new ShuffleOrder.UnshuffledShuffleOrder(2)); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 1)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - JSONArray items = arguments.getJSONArray(KEY_ITEMS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(1); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_ADD_ITEMS); - assertThat(arguments.getInt(KEY_INDEX)).isEqualTo(1); - assertThat(items.length()).isEqualTo(2); - } - - @Test - public void addItems_withoutItemOptionalFields_doesNotAddFieldsToJson() throws JSONException { - MediaItem itemWithoutOptionalFields = - new MediaItem.Builder() - .setTitle("title") - .setMimeType(MimeTypes.AUDIO_MP4) - .setDescription("desc") - .setDrmSchemes(Collections.singletonList(new DrmScheme(C.WIDEVINE_UUID, null))) - .setMedia("www.google.com") - .build(); - ExoCastMessage message = - new ExoCastMessage.AddItems( - C.INDEX_UNSET, - Collections.singletonList(itemWithoutOptionalFields), - new ShuffleOrder.UnshuffledShuffleOrder(1)); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - JSONArray items = arguments.getJSONArray(KEY_ITEMS); - - assertJsonEqualsMediaItem(items.getJSONObject(/* index= */ 0), itemWithoutOptionalFields); - } - - @Test - public void addItems_withAllItemFields_addsFieldsToJson() throws JSONException { - HashMap headersMedia = new HashMap<>(); - headersMedia.put("header1", "value1"); - headersMedia.put("header2", "value2"); - UriBundle media = new UriBundle(Uri.parse("www.google.com"), headersMedia); - - HashMap headersWidevine = new HashMap<>(); - headersWidevine.put("widevine", "value"); - UriBundle widevingUriBundle = new UriBundle(Uri.parse("www.widevine.com"), headersWidevine); - - HashMap headersPlayready = new HashMap<>(); - headersPlayready.put("playready", "value"); - UriBundle playreadyUriBundle = new UriBundle(Uri.parse("www.playready.com"), headersPlayready); - - DrmScheme[] drmSchemes = - new DrmScheme[] { - new DrmScheme(C.WIDEVINE_UUID, widevingUriBundle), - new DrmScheme(C.PLAYREADY_UUID, playreadyUriBundle) - }; - MediaItem itemWithAllFields = - new MediaItem.Builder() - .setTitle("title") - .setMimeType(MimeTypes.VIDEO_MP4) - .setDescription("desc") - .setStartPositionUs(3) - .setEndPositionUs(10) - .setDrmSchemes(Arrays.asList(drmSchemes)) - .setMedia(media) - .build(); - ExoCastMessage message = - new ExoCastMessage.AddItems( - C.INDEX_UNSET, - Collections.singletonList(itemWithAllFields), - new ShuffleOrder.UnshuffledShuffleOrder(1)); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - JSONArray items = arguments.getJSONArray(KEY_ITEMS); - - assertJsonEqualsMediaItem(items.getJSONObject(/* index= */ 0), itemWithAllFields); - } - - @Test - public void addItems_withShuffleOrder_producesExpectedJson() throws JSONException { - MediaItem.Builder builder = new MediaItem.Builder(); - MediaItem sampleItem1 = builder.build(); - MediaItem sampleItem2 = builder.build(); - MediaItem sampleItem3 = builder.build(); - MediaItem sampleItem4 = builder.build(); - - ExoCastMessage message = - new ExoCastMessage.AddItems( - C.INDEX_UNSET, - Arrays.asList(sampleItem1, sampleItem2, sampleItem3, sampleItem4), - new ShuffleOrder.DefaultShuffleOrder(new int[] {2, 1, 3, 0}, /* randomSeed= */ 0)); - JSONObject arguments = - new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)).getJSONObject(KEY_ARGS); - JSONArray shuffledIndices = arguments.getJSONArray(KEY_SHUFFLE_ORDER); - assertThat(shuffledIndices.getInt(0)).isEqualTo(2); - assertThat(shuffledIndices.getInt(1)).isEqualTo(1); - assertThat(shuffledIndices.getInt(2)).isEqualTo(3); - assertThat(shuffledIndices.getInt(3)).isEqualTo(0); - } - - @Test - public void moveItem_producesExpectedJson() throws JSONException { - ExoCastMessage message = - new ExoCastMessage.MoveItem( - new UUID(0, 1), - /* index= */ 3, - new ShuffleOrder.DefaultShuffleOrder(new int[] {2, 1, 3, 0}, /* randomSeed= */ 0)); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 1)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(1); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_MOVE_ITEM); - assertThat(arguments.getString(KEY_UUID)).isEqualTo(new UUID(0, 1).toString()); - assertThat(arguments.getInt(KEY_INDEX)).isEqualTo(3); - JSONArray shuffledIndices = arguments.getJSONArray(KEY_SHUFFLE_ORDER); - assertThat(shuffledIndices.getInt(0)).isEqualTo(2); - assertThat(shuffledIndices.getInt(1)).isEqualTo(1); - assertThat(shuffledIndices.getInt(2)).isEqualTo(3); - assertThat(shuffledIndices.getInt(3)).isEqualTo(0); - } - - @Test - public void removeItems_withSingleItem_producesExpectedJson() throws JSONException { - ExoCastMessage message = - new ExoCastMessage.RemoveItems(Collections.singletonList(new UUID(0, 1))); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONArray uuids = messageAsJson.getJSONObject(KEY_ARGS).getJSONArray(KEY_UUIDS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_REMOVE_ITEMS); - assertThat(uuids.length()).isEqualTo(1); - assertThat(uuids.getString(0)).isEqualTo(new UUID(0, 1).toString()); - } - - @Test - public void removeItems_withMultipleItems_producesExpectedJson() throws JSONException { - ExoCastMessage message = - new ExoCastMessage.RemoveItems( - Arrays.asList(new UUID(0, 1), new UUID(0, 2), new UUID(0, 3))); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONArray uuids = messageAsJson.getJSONObject(KEY_ARGS).getJSONArray(KEY_UUIDS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_REMOVE_ITEMS); - assertThat(uuids.length()).isEqualTo(3); - assertThat(uuids.getString(0)).isEqualTo(new UUID(0, 1).toString()); - assertThat(uuids.getString(1)).isEqualTo(new UUID(0, 2).toString()); - assertThat(uuids.getString(2)).isEqualTo(new UUID(0, 3).toString()); - } - - @Test - public void setPlayWhenReady_producesExpectedJson() throws JSONException { - ExoCastMessage message = new ExoCastMessage.SetPlayWhenReady(true); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_PLAY_WHEN_READY); - assertThat(messageAsJson.getJSONObject(KEY_ARGS).getBoolean(KEY_PLAY_WHEN_READY)).isTrue(); - } - - @Test - public void setRepeatMode_withRepeatModeOff_producesExpectedJson() throws JSONException { - ExoCastMessage message = new ExoCastMessage.SetRepeatMode(Player.REPEAT_MODE_OFF); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_REPEAT_MODE); - assertThat(messageAsJson.getJSONObject(KEY_ARGS).getString(KEY_REPEAT_MODE)) - .isEqualTo(STR_REPEAT_MODE_OFF); - } - - @Test - public void setRepeatMode_withRepeatModeOne_producesExpectedJson() throws JSONException { - ExoCastMessage message = new ExoCastMessage.SetRepeatMode(Player.REPEAT_MODE_ONE); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_REPEAT_MODE); - assertThat(messageAsJson.getJSONObject(KEY_ARGS).getString(KEY_REPEAT_MODE)) - .isEqualTo(STR_REPEAT_MODE_ONE); - } - - @Test - public void setRepeatMode_withRepeatModeAll_producesExpectedJson() throws JSONException { - ExoCastMessage message = new ExoCastMessage.SetRepeatMode(Player.REPEAT_MODE_ALL); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_REPEAT_MODE); - assertThat(messageAsJson.getJSONObject(KEY_ARGS).getString(KEY_REPEAT_MODE)) - .isEqualTo(STR_REPEAT_MODE_ALL); - } - - @Test - public void setShuffleModeEnabled_producesExpectedJson() throws JSONException { - ExoCastMessage message = - new ExoCastMessage.SetShuffleModeEnabled(/* shuffleModeEnabled= */ false); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_SHUFFLE_MODE_ENABLED); - assertThat(messageAsJson.getJSONObject(KEY_ARGS).getBoolean(KEY_SHUFFLE_MODE_ENABLED)) - .isFalse(); - } - - @Test - public void seekTo_withPositionInItem_addsPositionField() throws JSONException { - ExoCastMessage message = new ExoCastMessage.SeekTo(new UUID(0, 1), /* positionMs= */ 10); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SEEK_TO); - assertThat(arguments.getString(KEY_UUID)).isEqualTo(new UUID(0, 1).toString()); - assertThat(arguments.getLong(KEY_POSITION_MS)).isEqualTo(10); - } - - @Test - public void seekTo_withUnsetPosition_doesNotAddPositionField() throws JSONException { - ExoCastMessage message = - new ExoCastMessage.SeekTo(new UUID(0, 1), /* positionMs= */ C.TIME_UNSET); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SEEK_TO); - assertThat(arguments.getString(KEY_UUID)).isEqualTo(new UUID(0, 1).toString()); - assertThat(arguments.has(KEY_POSITION_MS)).isFalse(); - } - - @Test - public void setPlaybackParameters_producesExpectedJson() throws JSONException { - ExoCastMessage message = - new ExoCastMessage.SetPlaybackParameters( - new PlaybackParameters(/* speed= */ 0.5f, /* pitch= */ 2, /* skipSilence= */ false)); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_PLAYBACK_PARAMETERS); - assertThat(arguments.getDouble(KEY_SPEED)).isEqualTo(0.5); - assertThat(arguments.getDouble(KEY_PITCH)).isEqualTo(2.0); - assertThat(arguments.getBoolean(KEY_SKIP_SILENCE)).isFalse(); - } - - @Test - public void setSelectionParameters_producesExpectedJson() throws JSONException { - ExoCastMessage message = - new ExoCastMessage.SetTrackSelectionParameters( - TrackSelectionParameters.DEFAULT - .buildUpon() - .setDisabledTextTrackSelectionFlags( - C.SELECTION_FLAG_AUTOSELECT | C.SELECTION_FLAG_DEFAULT) - .setSelectUndeterminedTextLanguage(true) - .setPreferredAudioLanguage("esp") - .setPreferredTextLanguage("deu") - .build()); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)) - .isEqualTo(METHOD_SET_TRACK_SELECTION_PARAMETERS); - assertThat(arguments.getBoolean(KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE)).isTrue(); - assertThat(arguments.getString(KEY_PREFERRED_AUDIO_LANGUAGE)).isEqualTo("esp"); - assertThat(arguments.getString(KEY_PREFERRED_TEXT_LANGUAGE)).isEqualTo("deu"); - ArrayList selectionFlagStrings = new ArrayList<>(); - JSONArray selectionFlagsJson = arguments.getJSONArray(KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS); - for (int i = 0; i < selectionFlagsJson.length(); i++) { - selectionFlagStrings.add(selectionFlagsJson.getString(i)); - } - assertThat(selectionFlagStrings).contains(ExoCastConstants.STR_SELECTION_FLAG_AUTOSELECT); - assertThat(selectionFlagStrings).doesNotContain(ExoCastConstants.STR_SELECTION_FLAG_FORCED); - assertThat(selectionFlagStrings).contains(ExoCastConstants.STR_SELECTION_FLAG_DEFAULT); - } - - private static void assertJsonEqualsMediaItem(JSONObject itemAsJson, MediaItem mediaItem) - throws JSONException { - assertThat(itemAsJson.getString(KEY_UUID)).isEqualTo(mediaItem.uuid.toString()); - assertThat(itemAsJson.getString(KEY_TITLE)).isEqualTo(mediaItem.title); - assertThat(itemAsJson.getString(KEY_MIME_TYPE)).isEqualTo(mediaItem.mimeType); - assertThat(itemAsJson.getString(KEY_DESCRIPTION)).isEqualTo(mediaItem.description); - assertJsonMatchesTimestamp(itemAsJson, KEY_START_POSITION_US, mediaItem.startPositionUs); - assertJsonMatchesTimestamp(itemAsJson, KEY_END_POSITION_US, mediaItem.endPositionUs); - assertJsonMatchesUriBundle(itemAsJson, KEY_MEDIA, mediaItem.media); - - List drmSchemes = mediaItem.drmSchemes; - int drmSchemesLength = drmSchemes.size(); - JSONArray drmSchemesAsJson = itemAsJson.getJSONArray(KEY_DRM_SCHEMES); - - assertThat(drmSchemesAsJson.length()).isEqualTo(drmSchemesLength); - for (int i = 0; i < drmSchemesLength; i++) { - DrmScheme drmScheme = drmSchemes.get(i); - JSONObject drmSchemeAsJson = drmSchemesAsJson.getJSONObject(i); - - assertThat(drmSchemeAsJson.getString(KEY_UUID)).isEqualTo(drmScheme.uuid.toString()); - assertJsonMatchesUriBundle(drmSchemeAsJson, KEY_LICENSE_SERVER, drmScheme.licenseServer); - } - } - - private static void assertJsonMatchesUriBundle( - JSONObject jsonObject, String key, @Nullable UriBundle uriBundle) throws JSONException { - if (uriBundle == null) { - assertThat(jsonObject.has(key)).isFalse(); - return; - } - JSONObject uriBundleAsJson = jsonObject.getJSONObject(key); - assertThat(uriBundleAsJson.getString(KEY_URI)).isEqualTo(uriBundle.uri.toString()); - Map requestHeaders = uriBundle.requestHeaders; - JSONObject requestHeadersAsJson = uriBundleAsJson.getJSONObject(KEY_REQUEST_HEADERS); - - assertThat(requestHeadersAsJson.length()).isEqualTo(requestHeaders.size()); - for (String headerKey : requestHeaders.keySet()) { - assertThat(requestHeadersAsJson.getString(headerKey)) - .isEqualTo(requestHeaders.get(headerKey)); - } - } - - private static void assertJsonMatchesTimestamp(JSONObject object, String key, long timestamp) - throws JSONException { - if (timestamp == C.TIME_UNSET) { - assertThat(object.has(key)).isFalse(); - } else { - assertThat(object.getLong(key)).isEqualTo(timestamp); - } - } -} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayerTest.java deleted file mode 100644 index 58f78b090a..0000000000 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayerTest.java +++ /dev/null @@ -1,1018 +0,0 @@ -/* - * 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.ext.cast; - -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ARGS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_INDEX; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ITEMS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUIDS; -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isNull; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.testutil.FakeClock; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.UUID; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; - -/** Unit test for {@link ExoCastPlayer}. */ -@RunWith(AndroidJUnit4.class) -public class ExoCastPlayerTest { - - private static final long MOCK_SEQUENCE_NUMBER = 1; - private ExoCastPlayer player; - private MediaItem.Builder itemBuilder; - private CastSessionManager.StateListener receiverAppStateListener; - private FakeClock clock; - @Mock private CastSessionManager sessionManager; - @Mock private SessionAvailabilityListener sessionAvailabilityListener; - @Mock private Player.EventListener playerEventListener; - - @Before - public void setUp() { - initMocks(this); - clock = new FakeClock(/* initialTimeMs= */ 0); - player = - new ExoCastPlayer( - listener -> { - receiverAppStateListener = listener; - return sessionManager; - }, - clock); - player.addListener(playerEventListener); - itemBuilder = new MediaItem.Builder(); - } - - @Test - public void exoCastPlayer_startsAndStopsSessionManager() { - // The session manager should have been started when setting up, with the creation of - // ExoCastPlayer. - verify(sessionManager).start(); - verifyNoMoreInteractions(sessionManager); - player.release(); - verify(sessionManager).stopTrackingSession(); - verifyNoMoreInteractions(sessionManager); - } - - @Test - public void exoCastPlayer_propagatesSessionStatus() { - player.setSessionAvailabilityListener(sessionAvailabilityListener); - verify(sessionAvailabilityListener, never()).onCastSessionAvailable(); - receiverAppStateListener.onCastSessionAvailable(); - verify(sessionAvailabilityListener).onCastSessionAvailable(); - verifyNoMoreInteractions(sessionAvailabilityListener); - receiverAppStateListener.onCastSessionUnavailable(); - verify(sessionAvailabilityListener).onCastSessionUnavailable(); - verifyNoMoreInteractions(sessionAvailabilityListener); - } - - @Test - public void addItemsToQueue_producesExpectedMessages() throws JSONException { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); - MediaItem item5 = itemBuilder.setUuid(toUuid(5)).build(); - - player.addItemsToQueue(item1, item2); - assertMediaItemQueue(item1, item2); - - player.addItemsToQueue(1, item3, item4); - assertMediaItemQueue(item1, item3, item4, item2); - - player.addItemsToQueue(item5); - assertMediaItemQueue(item1, item3, item4, item2, item5); - - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); - verify(sessionManager, times(3)).send(messageCaptor.capture()); - assertMessageAddsItems( - /* message= */ messageCaptor.getAllValues().get(0), - /* index= */ C.INDEX_UNSET, - Arrays.asList(item1, item2)); - assertMessageAddsItems( - /* message= */ messageCaptor.getAllValues().get(1), - /* index= */ 1, - Arrays.asList(item3, item4)); - assertMessageAddsItems( - /* message= */ messageCaptor.getAllValues().get(2), - /* index= */ C.INDEX_UNSET, - Collections.singletonList(item5)); - } - - @Test - public void addItemsToQueue_masksRemoteUpdates() { - player.prepare(); - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); - - player.addItemsToQueue(item1, item2); - assertMediaItemQueue(item1, item2); - - // Should be ignored due to a lower sequence number. - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) - .setItems(Arrays.asList(item3, item4)) - .build()); - - // Should override the current state. - assertMediaItemQueue(item1, item2); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) - .setItems(Arrays.asList(item3, item4)) - .build()); - - assertMediaItemQueue(item3, item4); - } - - @Test - public void addItemsToQueue_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 2, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(2); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(2); - player.addItemsToQueue(/* optionalIndex= */ 0, itemBuilder.build()); - assertThat(player.getCurrentWindowIndex()).isEqualTo(3); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(3); - - player.addItemsToQueue(itemBuilder.build()); - assertThat(player.getCurrentWindowIndex()).isEqualTo(3); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(3); - } - - @Test - public void addItemsToQueue_doesNotAddDuplicateUuids() { - player.prepare(); - player.addItemsToQueue(itemBuilder.setUuid(toUuid(1)).build()); - assertThat(player.getQueueSize()).isEqualTo(1); - player.addItemsToQueue( - itemBuilder.setUuid(toUuid(1)).build(), itemBuilder.setUuid(toUuid(2)).build()); - assertThat(player.getQueueSize()).isEqualTo(2); - try { - player.addItemsToQueue( - itemBuilder.setUuid(toUuid(3)).build(), itemBuilder.setUuid(toUuid(3)).build()); - fail(); - } catch (IllegalArgumentException e) { - // Expected. - } - } - - @Test - public void moveItemInQueue_behavesAsExpected() throws JSONException { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - player.addItemsToQueue(item1, item2, item3); - assertMediaItemQueue(item1, item2, item3); - player.moveItemInQueue(/* index= */ 0, /* newIndex= */ 2); - assertMediaItemQueue(item2, item3, item1); - player.moveItemInQueue(/* index= */ 1, /* newIndex= */ 1); - assertMediaItemQueue(item2, item3, item1); - player.moveItemInQueue(/* index= */ 1, /* newIndex= */ 0); - assertMediaItemQueue(item3, item2, item1); - - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); - verify(sessionManager, times(4)).send(messageCaptor.capture()); - // First sent message is an "add" message. - assertMessageMovesItem( - /* message= */ messageCaptor.getAllValues().get(1), item1, /* index= */ 2); - assertMessageMovesItem( - /* message= */ messageCaptor.getAllValues().get(2), item3, /* index= */ 1); - assertMessageMovesItem( - /* message= */ messageCaptor.getAllValues().get(3), item3, /* index= */ 0); - } - - @Test - public void moveItemInQueue_moveBeforeToAfter_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - player.moveItemInQueue(/* index= */ 0, /* newIndex= */ 1); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - } - - @Test - public void moveItemInQueue_moveAfterToBefore_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - player.moveItemInQueue(/* index= */ 1, /* newIndex= */ 0); - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - } - - @Test - public void moveItemInQueue_moveCurrent_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - player.moveItemInQueue(/* index= */ 0, /* newIndex= */ 2); - assertThat(player.getCurrentWindowIndex()).isEqualTo(2); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(2); - } - - @Test - public void removeItemsFromQueue_masksMediaQueue() throws JSONException { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); - MediaItem item5 = itemBuilder.setUuid(toUuid(5)).build(); - player.addItemsToQueue(item1, item2, item3, item4, item5); - assertMediaItemQueue(item1, item2, item3, item4, item5); - - player.removeItemFromQueue(2); - assertMediaItemQueue(item1, item2, item4, item5); - - player.removeRangeFromQueue(1, 3); - assertMediaItemQueue(item1, item5); - - player.clearQueue(); - assertMediaItemQueue(); - - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); - verify(sessionManager, times(4)).send(messageCaptor.capture()); - // First sent message is an "add" message. - assertMessageRemovesItems( - messageCaptor.getAllValues().get(1), Collections.singletonList(item3)); - assertMessageRemovesItems(messageCaptor.getAllValues().get(2), Arrays.asList(item2, item4)); - assertMessageRemovesItems(messageCaptor.getAllValues().get(3), Arrays.asList(item1, item5)); - } - - @Test - public void removeRangeFromQueue_beforeCurrentItem_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 2, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(2); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(2); - player.removeRangeFromQueue(/* indexFrom= */ 0, /* indexExclusiveTo= */ 2); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - } - - @Test - public void removeRangeFromQueue_currentItem_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - player.removeRangeFromQueue(/* indexFrom= */ 0, /* indexExclusiveTo= */ 2); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - } - - @Test - public void removeRangeFromQueue_currentItemWhichIsLast_transitionsToEnded() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - player.removeRangeFromQueue(/* indexFrom= */ 1, /* indexExclusiveTo= */ 3); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); - } - - @Test - public void clearQueue_resetsPlaybackPosition() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - player.clearQueue(); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); - } - - @Test - public void prepare_emptyQueue_transitionsToEnded() { - player.prepare(); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); - verify(playerEventListener).onPlayerStateChanged(/* playWhenReady=*/ false, Player.STATE_ENDED); - } - - @Test - public void prepare_withQueue_transitionsToBuffering() { - player.addItemsToQueue(itemBuilder.build()); - player.prepare(); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_BUFFERING); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady=*/ false, Player.STATE_BUFFERING); - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Timeline.class); - verify(playerEventListener) - .onTimelineChanged( - argumentCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_PREPARED)); - assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(1); - } - - @Test - public void stop_withoutReset_leavesCurrentTimeline() { - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); - player.addItemsToQueue(itemBuilder.setUuid(toUuid(1)).build()); - player.prepare(); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_BUFFERING); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_BUFFERING); - verify(playerEventListener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - player.stop(/* reset= */ false); - verify(playerEventListener).onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_IDLE); - - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Timeline.class); - // Update for prepare. - verify(playerEventListener) - .onTimelineChanged( - argumentCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_PREPARED)); - assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(1); - - // Update for stop. - verifyNoMoreInteractions(playerEventListener); - assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(1); - } - - @Test - public void stop_withReset_clearsQueue() { - player.prepare(); - player.addItemsToQueue(itemBuilder.setUuid(toUuid(1)).build()); - verify(playerEventListener) - .onTimelineChanged( - any(Timeline.class), isNull(), eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_BUFFERING); - player.stop(/* reset= */ true); - verify(playerEventListener).onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_IDLE); - - // Update for add. - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Timeline.class); - verify(playerEventListener) - .onTimelineChanged( - argumentCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); - assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(1); - - // Update for stop. - verify(playerEventListener) - .onTimelineChanged( - argumentCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_RESET)); - assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(0); - - assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); - } - - @Test - public void getCurrentTimeline_masksRemoteUpdates() { - player.prepare(); - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); - player.addItemsToQueue(item1, item2); - - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Timeline.class); - verify(playerEventListener) - .onTimelineChanged( - messageCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); - Timeline reportedTimeline = messageCaptor.getValue(); - assertThat(reportedTimeline).isSameInstanceAs(player.getCurrentTimeline()); - assertThat(reportedTimeline.getWindowCount()).isEqualTo(2); - assertThat(reportedTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).durationUs) - .isEqualTo(C.TIME_UNSET); - assertThat(reportedTimeline.getWindow(/* windowIndex= */ 1, new Timeline.Window()).durationUs) - .isEqualTo(C.TIME_UNSET); - } - - @Test - public void getCurrentTimeline_exposesReceiverState() { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_BUFFERING) - .setItems(Arrays.asList(item1, item2)) - .setShuffleOrder(Arrays.asList(1, 0)) - .build()); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Timeline.class); - verify(playerEventListener) - .onTimelineChanged( - messageCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); - Timeline reportedTimeline = messageCaptor.getValue(); - assertThat(reportedTimeline).isSameInstanceAs(player.getCurrentTimeline()); - assertThat(reportedTimeline.getWindowCount()).isEqualTo(2); - assertThat(reportedTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).durationUs) - .isEqualTo(C.TIME_UNSET); - assertThat(reportedTimeline.getWindow(/* windowIndex= */ 1, new Timeline.Window()).durationUs) - .isEqualTo(C.TIME_UNSET); - } - - @Test - public void timelineUpdateFromReceiver_matchesLocalState_doesNotCallEventLsitener() { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); - - MediaItemInfo.Period period1 = - new MediaItemInfo.Period("id1", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 0); - MediaItemInfo.Period period2 = - new MediaItemInfo.Period( - "id2", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 1000000L); - MediaItemInfo.Period period3 = - new MediaItemInfo.Period( - "id3", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 2000000L); - HashMap mediaItemInfoMap1 = new HashMap<>(); - mediaItemInfoMap1.put( - toUuid(1), - new MediaItemInfo( - /* windowDurationUs= */ 3000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2, period3), - /* positionInFirstPeriodUs= */ 0, - /* isSeekable= */ true, - /* isDynamic= */ false)); - mediaItemInfoMap1.put( - toUuid(3), - new MediaItemInfo( - /* windowDurationUs= */ 2000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 500, - /* isSeekable= */ true, - /* isDynamic= */ false)); - - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(1) - .setPlaybackState(Player.STATE_BUFFERING) - .setItems(Arrays.asList(item1, item2, item3, item4)) - .setShuffleOrder(Arrays.asList(1, 0, 2, 3)) - .setMediaItemsInformation(mediaItemInfoMap1) - .build()); - verify(playerEventListener) - .onTimelineChanged( - any(), /* manifest= */ isNull(), eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); - verify(playerEventListener) - .onPlayerStateChanged( - /* playWhenReady= */ false, /* playbackState= */ Player.STATE_BUFFERING); - - HashMap mediaItemInfoMap2 = new HashMap<>(mediaItemInfoMap1); - mediaItemInfoMap2.put( - toUuid(5), - new MediaItemInfo( - /* windowDurationUs= */ 5, - /* defaultStartPositionUs= */ 0, - /* periods= */ Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 500, - /* isSeekable= */ true, - /* isDynamic= */ false)); - - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(1).setMediaItemsInformation(mediaItemInfoMap2).build()); - verifyNoMoreInteractions(playerEventListener); - } - - @Test - public void getPeriodIndex_producesExpectedOutput() { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); - - MediaItemInfo.Period period1 = - new MediaItemInfo.Period("id1", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 0); - MediaItemInfo.Period period2 = - new MediaItemInfo.Period( - "id2", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 1000000L); - MediaItemInfo.Period period3 = - new MediaItemInfo.Period( - "id3", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 2000000L); - HashMap mediaItemInfoMap = new HashMap<>(); - mediaItemInfoMap.put( - toUuid(1), - new MediaItemInfo( - /* windowDurationUs= */ 3000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2, period3), - /* positionInFirstPeriodUs= */ 0, - /* isSeekable= */ true, - /* isDynamic= */ false)); - mediaItemInfoMap.put( - toUuid(3), - new MediaItemInfo( - /* windowDurationUs= */ 2000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 500, - /* isSeekable= */ true, - /* isDynamic= */ false)); - - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1L) - .setPlaybackState(Player.STATE_BUFFERING) - .setItems(Arrays.asList(item1, item2, item3, item4)) - .setShuffleOrder(Arrays.asList(1, 0, 3, 2)) - .setMediaItemsInformation(mediaItemInfoMap) - .setPlaybackPosition( - /* currentPlayingItemUuid= */ item3.uuid, - /* currentPlayingPeriodId= */ "id2", - /* currentPlaybackPositionMs= */ 500L) - .build()); - - assertThat(player.getCurrentPeriodIndex()).isEqualTo(5); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0L); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(3); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1500L); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - } - - @Test - public void exoCastPlayer_propagatesPlayerStateFromReceiver() { - ReceiverAppStateUpdate.Builder builder = - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1); - - // The first idle state update should be discarded, since it matches the current state. - receiverAppStateListener.onStateUpdateFromReceiverApp( - builder.setPlaybackState(Player.STATE_IDLE).build()); - receiverAppStateListener.onStateUpdateFromReceiverApp( - builder.setPlaybackState(Player.STATE_BUFFERING).build()); - receiverAppStateListener.onStateUpdateFromReceiverApp( - builder.setPlaybackState(Player.STATE_READY).build()); - receiverAppStateListener.onStateUpdateFromReceiverApp( - builder.setPlaybackState(Player.STATE_ENDED).build()); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Integer.class); - verify(playerEventListener, times(3)) - .onPlayerStateChanged(/* playWhenReady= */ eq(false), messageCaptor.capture()); - List states = messageCaptor.getAllValues(); - assertThat(states).hasSize(3); - assertThat(states) - .isEqualTo(Arrays.asList(Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED)); - } - - @Test - public void setPlayWhenReady_changedLocally_notifiesListeners() { - player.setPlayWhenReady(false); - verify(playerEventListener, never()).onPlayerStateChanged(false, Player.STATE_IDLE); - player.setPlayWhenReady(true); - verify(playerEventListener).onPlayerStateChanged(true, Player.STATE_IDLE); - player.setPlayWhenReady(false); - verify(playerEventListener).onPlayerStateChanged(false, Player.STATE_IDLE); - } - - @Test - public void setPlayWhenReady_changedRemotely_notifiesListeners() { - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0).setPlayWhenReady(true).build()); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady= */ true, /* playbackState= */ Player.STATE_IDLE); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0).setPlayWhenReady(true).build()); - verifyNoMoreInteractions(playerEventListener); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0).setPlayWhenReady(false).build()); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); - verifyNoMoreInteractions(playerEventListener); - } - - @Test - public void getPlayWhenReady_masksRemoteUpdates() { - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - player.setPlayWhenReady(true); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady= */ true, /* playbackState= */ Player.STATE_IDLE); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2).setPlayWhenReady(false).build()); - verifyNoMoreInteractions(playerEventListener); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3).setPlayWhenReady(true).build()); - verifyNoMoreInteractions(playerEventListener); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3).setPlayWhenReady(false).build()); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); - } - - @Test - public void setRepeatMode_changedLocally_notifiesListeners() { - player.setRepeatMode(Player.REPEAT_MODE_OFF); - verifyNoMoreInteractions(playerEventListener); - player.setRepeatMode(Player.REPEAT_MODE_ONE); - verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); - player.setRepeatMode(Player.REPEAT_MODE_ONE); - verifyNoMoreInteractions(playerEventListener); - } - - @Test - public void setRepeatMode_changedRemotely_notifiesListeners() { - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0) - .setRepeatMode(Player.REPEAT_MODE_ONE) - .build()); - verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); - assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE); - } - - @Test - public void getRepeatMode_masksRemoteUpdates() { - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - player.setRepeatMode(Player.REPEAT_MODE_ALL); - assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); - verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) - .setRepeatMode(Player.REPEAT_MODE_ONE) - .build()); - verifyNoMoreInteractions(playerEventListener); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) - .setRepeatMode(Player.REPEAT_MODE_ONE) - .build()); - verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); - } - - @Test - public void getPlaybackPosition_withStateChanges_producesExpectedOutput() { - UUID uuid = toUuid(1); - HashMap mediaItemInfoMap = new HashMap<>(); - - MediaItemInfo.Period period1 = new MediaItemInfo.Period("id1", 1000L, 0); - MediaItemInfo.Period period2 = new MediaItemInfo.Period("id2", 1000L, 0); - MediaItemInfo.Period period3 = new MediaItemInfo.Period("id3", 1000L, 0); - mediaItemInfoMap.put( - uuid, - new MediaItemInfo( - /* windowDurationUs= */ 1000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2, period3), - /* positionInFirstPeriodUs= */ 500, - /* isSeekable= */ true, - /* isDynamic= */ false)); - - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(1L); - player.addItemsToQueue(itemBuilder.setUuid(uuid).build()); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_BUFFERING) - .setMediaItemsInformation(mediaItemInfoMap) - .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1000L) - .build()); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPosition()).isEqualTo(1000L); - clock.advanceTime(/* timeDiffMs= */ 1L); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_READY) - .build()); - // Play when ready is still false, so position should not change. - assertThat(player.getCurrentPosition()).isEqualTo(1000L); - player.setPlayWhenReady(true); - clock.advanceTime(1); - assertThat(player.getCurrentPosition()).isEqualTo(1001L); - clock.advanceTime(1); - assertThat(player.getCurrentPosition()).isEqualTo(1002L); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_BUFFERING) - .setMediaItemsInformation(mediaItemInfoMap) - .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1010L) - .build()); - clock.advanceTime(1); - assertThat(player.getCurrentPosition()).isEqualTo(1010L); - clock.advanceTime(1); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_READY) - .setMediaItemsInformation(mediaItemInfoMap) - .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1011L) - .build()); - clock.advanceTime(10); - assertThat(player.getCurrentPosition()).isEqualTo(1021L); - } - - @Test - public void getPlaybackPosition_withNonDefaultPlaybackSpeed_producesExpectedOutput() { - MediaItem item = itemBuilder.setUuid(toUuid(1)).build(); - MediaItemInfo info = - new MediaItemInfo( - /* windowDurationUs= */ 10000000, - /* defaultStartPositionUs= */ 3000000, - /* periods= */ Collections.singletonList( - new MediaItemInfo.Period( - /* id= */ "id", /* durationUs= */ 10000000, /* positionInWindowUs= */ 0)), - /* positionInFirstPeriodUs= */ 0, - /* isSeekable= */ true, - /* isDynamic= */ false); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setMediaItemsInformation(Collections.singletonMap(toUuid(1), info)) - .setShuffleOrder(Collections.singletonList(0)) - .setItems(Collections.singletonList(item)) - .setPlaybackPosition( - toUuid(1), /* currentPlayingPeriodId= */ "id", /* currentPlaybackPositionMs= */ 20L) - .setPlaybackState(Player.STATE_READY) - .setPlayWhenReady(true) - .build()); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPosition()).isEqualTo(20); - clock.advanceTime(10); - assertThat(player.getCurrentPosition()).isEqualTo(30); - clock.advanceTime(10); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(1) - .setPlaybackPosition( - toUuid(1), /* currentPlayingPeriodId= */ "id", /* currentPlaybackPositionMs= */ 40L) - .setPlaybackParameters(new PlaybackParameters(2)) - .build()); - clock.advanceTime(10); - assertThat(player.getCurrentPosition()).isEqualTo(60); - } - - @Test - public void positionChanges_notifiesDiscontinuities() { - UUID uuid = toUuid(1); - HashMap mediaItemInfoMap = new HashMap<>(); - - MediaItemInfo.Period period1 = new MediaItemInfo.Period("id1", 1000L, 0); - MediaItemInfo.Period period2 = new MediaItemInfo.Period("id2", 1000L, 0); - MediaItemInfo.Period period3 = new MediaItemInfo.Period("id3", 1000L, 0); - mediaItemInfoMap.put( - uuid, - new MediaItemInfo( - /* windowDurationUs= */ 1000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2, period3), - /* positionInFirstPeriodUs= */ 500, - /* isSeekable= */ true, - /* isDynamic= */ false)); - - player.addItemsToQueue(itemBuilder.setUuid(uuid).build()); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_BUFFERING) - .setMediaItemsInformation(mediaItemInfoMap) - .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1000L) - .setDiscontinuityReason(Player.DISCONTINUITY_REASON_SEEK) - .build()); - verify(playerEventListener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 999); - verify(playerEventListener, times(2)).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - } - - @Test - public void setShuffleModeEnabled_changedLocally_notifiesListeners() { - player.setShuffleModeEnabled(true); - verify(playerEventListener).onShuffleModeEnabledChanged(true); - player.setShuffleModeEnabled(true); - verifyNoMoreInteractions(playerEventListener); - } - - @Test - public void setShuffleModeEnabled_changedRemotely_notifiesListeners() { - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0) - .setShuffleModeEnabled(true) - .build()); - verify(playerEventListener).onShuffleModeEnabledChanged(true); - assertThat(player.getShuffleModeEnabled()).isTrue(); - } - - @Test - public void getShuffleMode_masksRemoteUpdates() { - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - player.setShuffleModeEnabled(true); - assertThat(player.getShuffleModeEnabled()).isTrue(); - verify(playerEventListener).onShuffleModeEnabledChanged(true); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) - .setShuffleModeEnabled(false) - .build()); - verifyNoMoreInteractions(playerEventListener); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) - .setShuffleModeEnabled(false) - .build()); - verify(playerEventListener).onShuffleModeEnabledChanged(false); - assertThat(player.getShuffleModeEnabled()).isFalse(); - } - - @Test - public void seekTo_inIdle_doesNotChangePlaybackState() { - player.prepare(); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build()); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 0); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_BUFFERING); - player.stop(false); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 0); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); - } - - @Test - public void seekTo_withTwoItems_producesExpectedMessage() { - player.prepare(); - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - player.addItemsToQueue(item1, item2); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1000); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); - verify(sessionManager, times(3)).send(messageCaptor.capture()); - // Messages should be prepare, add and seek. - ExoCastMessage.SeekTo seekToMessage = - (ExoCastMessage.SeekTo) messageCaptor.getAllValues().get(2); - assertThat(seekToMessage.positionMs).isEqualTo(1000); - assertThat(seekToMessage.uuid).isEqualTo(toUuid(2)); - } - - @Test - public void seekTo_masksRemoteUpdates() { - player.prepare(); - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - player.addItemsToQueue(item1, item2); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1000L); - verify(playerEventListener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - verify(playerEventListener) - .onPlayerStateChanged( - /* playWhenReady= */ false, /* playbackState= */ Player.STATE_BUFFERING); - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPosition()).isEqualTo(1000); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) - .setPlaybackPosition(toUuid(1), "id", 500L) - .build()); - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPosition()).isEqualTo(1000); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) - .setPlaybackPosition(toUuid(1), "id", 500L) - .setDiscontinuityReason(Player.DISCONTINUITY_REASON_SEEK) - .build()); - verify(playerEventListener, times(2)).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPosition()).isEqualTo(500); - } - - @Test - public void setPlaybackParameters_producesExpectedMessage() { - PlaybackParameters playbackParameters = - new PlaybackParameters(/* speed= */ .5f, /* pitch= */ .25f, /* skipSilence= */ true); - player.setPlaybackParameters(playbackParameters); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); - verify(sessionManager).send(messageCaptor.capture()); - ExoCastMessage.SetPlaybackParameters message = - (ExoCastMessage.SetPlaybackParameters) messageCaptor.getValue(); - assertThat(message.playbackParameters).isEqualTo(playbackParameters); - } - - @Test - public void getTrackSelectionParameters_doesNotOverrideUnexpectedFields() { - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - DefaultTrackSelector.Parameters parameters = - DefaultTrackSelector.Parameters.DEFAULT - .buildUpon() - .setPreferredAudioLanguage("spa") - .setMaxVideoSize(/* maxVideoWidth= */ 3, /* maxVideoHeight= */ 3) - .build(); - player.setTrackSelectionParameters(parameters); - TrackSelectionParameters returned = - TrackSelectionParameters.DEFAULT.buildUpon().setPreferredAudioLanguage("deu").build(); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) - .setTrackSelectionParameters(returned) - .build()); - DefaultTrackSelector.Parameters result = - (DefaultTrackSelector.Parameters) player.getTrackSelectionParameters(); - assertThat(result.preferredAudioLanguage).isEqualTo("deu"); - assertThat(result.maxVideoHeight).isEqualTo(3); - assertThat(result.maxVideoWidth).isEqualTo(3); - } - - @Test - public void testExoCast_getRendererType() { - assertThat(player.getRendererCount()).isEqualTo(4); - assertThat(player.getRendererType(/* index= */ 0)).isEqualTo(C.TRACK_TYPE_VIDEO); - assertThat(player.getRendererType(/* index= */ 1)).isEqualTo(C.TRACK_TYPE_AUDIO); - assertThat(player.getRendererType(/* index= */ 2)).isEqualTo(C.TRACK_TYPE_TEXT); - assertThat(player.getRendererType(/* index= */ 3)).isEqualTo(C.TRACK_TYPE_METADATA); - } - - private static UUID toUuid(long lowerBits) { - return new UUID(0, lowerBits); - } - - private void assertMediaItemQueue(MediaItem... mediaItemQueue) { - assertThat(player.getQueueSize()).isEqualTo(mediaItemQueue.length); - for (int i = 0; i < mediaItemQueue.length; i++) { - assertThat(player.getQueueItem(i).uuid).isEqualTo(mediaItemQueue[i].uuid); - } - } - - private static void assertMessageAddsItems( - ExoCastMessage message, int index, List mediaItems) throws JSONException { - assertThat(message.method).isEqualTo(ExoCastConstants.METHOD_ADD_ITEMS); - JSONObject args = - new JSONObject(message.toJsonString(MOCK_SEQUENCE_NUMBER)).getJSONObject(KEY_ARGS); - if (index != C.INDEX_UNSET) { - assertThat(args.getInt(KEY_INDEX)).isEqualTo(index); - } else { - assertThat(args.has(KEY_INDEX)).isFalse(); - } - JSONArray itemsAsJson = args.getJSONArray(KEY_ITEMS); - assertThat(ReceiverAppStateUpdate.toMediaItemArrayList(itemsAsJson)).isEqualTo(mediaItems); - } - - private static void assertMessageMovesItem(ExoCastMessage message, MediaItem item, int index) - throws JSONException { - assertThat(message.method).isEqualTo(ExoCastConstants.METHOD_MOVE_ITEM); - JSONObject args = - new JSONObject(message.toJsonString(MOCK_SEQUENCE_NUMBER)).getJSONObject(KEY_ARGS); - assertThat(args.getString(KEY_UUID)).isEqualTo(item.uuid.toString()); - assertThat(args.getInt(KEY_INDEX)).isEqualTo(index); - } - - private static void assertMessageRemovesItems(ExoCastMessage message, List items) - throws JSONException { - assertThat(message.method).isEqualTo(ExoCastConstants.METHOD_REMOVE_ITEMS); - JSONObject args = - new JSONObject(message.toJsonString(MOCK_SEQUENCE_NUMBER)).getJSONObject(KEY_ARGS); - JSONArray uuidsAsJson = args.getJSONArray(KEY_UUIDS); - for (int i = 0; i < uuidsAsJson.length(); i++) { - assertThat(uuidsAsJson.getString(i)).isEqualTo(items.get(i).uuid.toString()); - } - } -} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastTimelineTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastTimelineTest.java deleted file mode 100644 index f6084339e4..0000000000 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastTimelineTest.java +++ /dev/null @@ -1,466 +0,0 @@ -/* - * 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.ext.cast; - -import static com.google.common.truth.Truth.assertThat; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.UUID; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit test for {@link ExoCastTimeline}. */ -@RunWith(AndroidJUnit4.class) -public class ExoCastTimelineTest { - - private MediaItem mediaItem1; - private MediaItem mediaItem2; - private MediaItem mediaItem3; - private MediaItem mediaItem4; - private MediaItem mediaItem5; - - @Before - public void setUp() { - MediaItem.Builder builder = new MediaItem.Builder(); - mediaItem1 = builder.setUuid(asUUID(1)).build(); - mediaItem2 = builder.setUuid(asUUID(2)).build(); - mediaItem3 = builder.setUuid(asUUID(3)).build(); - mediaItem4 = builder.setUuid(asUUID(4)).build(); - mediaItem5 = builder.setUuid(asUUID(5)).build(); - } - - @Test - public void getWindowCount_withNoItems_producesExpectedCount() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Collections.emptyList(), Collections.emptyMap(), new DefaultShuffleOrder(0)); - - assertThat(timeline.getWindowCount()).isEqualTo(0); - } - - @Test - public void getWindowCount_withFiveItems_producesExpectedCount() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(5)); - - assertThat(timeline.getWindowCount()).isEqualTo(5); - } - - @Test - public void getWindow_withNoMediaItemInfo_returnsEmptyWindow() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(5)); - Timeline.Window window = timeline.getWindow(2, new Timeline.Window(), /* setTag= */ true); - - assertThat(window.tag).isNull(); - assertThat(window.presentationStartTimeMs).isEqualTo(C.TIME_UNSET); - assertThat(window.windowStartTimeMs).isEqualTo(C.TIME_UNSET); - assertThat(window.isSeekable).isFalse(); - assertThat(window.isDynamic).isTrue(); - assertThat(window.defaultPositionUs).isEqualTo(0L); - assertThat(window.durationUs).isEqualTo(C.TIME_UNSET); - assertThat(window.firstPeriodIndex).isEqualTo(2); - assertThat(window.lastPeriodIndex).isEqualTo(2); - assertThat(window.positionInFirstPeriodUs).isEqualTo(0L); - } - - @Test - public void getWindow_withMediaItemInfo_returnsPopulatedWindow() { - MediaItem populatedMediaItem = new MediaItem.Builder().setAttachment("attachment").build(); - HashMap mediaItemInfos = new HashMap<>(); - MediaItemInfo.Period period1 = - new MediaItemInfo.Period("id1", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 0L); - MediaItemInfo.Period period2 = - new MediaItemInfo.Period( - "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L); - mediaItemInfos.put( - populatedMediaItem.uuid, - new MediaItemInfo( - /* windowDurationUs= */ 4000000L, - /* defaultStartPositionUs= */ 20L, - Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 500L, - /* isSeekable= */ true, - /* isDynamic= */ false)); - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, populatedMediaItem), - mediaItemInfos, - new DefaultShuffleOrder(5)); - Timeline.Window window = timeline.getWindow(4, new Timeline.Window(), /* setTag= */ true); - - assertThat(window.tag).isSameInstanceAs(populatedMediaItem.attachment); - assertThat(window.presentationStartTimeMs).isEqualTo(C.TIME_UNSET); - assertThat(window.windowStartTimeMs).isEqualTo(C.TIME_UNSET); - assertThat(window.isSeekable).isTrue(); - assertThat(window.isDynamic).isFalse(); - assertThat(window.defaultPositionUs).isEqualTo(20L); - assertThat(window.durationUs).isEqualTo(4000000L); - assertThat(window.firstPeriodIndex).isEqualTo(4); - assertThat(window.lastPeriodIndex).isEqualTo(5); - assertThat(window.positionInFirstPeriodUs).isEqualTo(500L); - } - - @Test - public void getPeriodCount_producesExpectedOutput() { - HashMap mediaItemInfos = new HashMap<>(); - MediaItemInfo.Period period1 = - new MediaItemInfo.Period( - "id1", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L); - MediaItemInfo.Period period2 = - new MediaItemInfo.Period( - "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 6000000L); - mediaItemInfos.put( - asUUID(2), - new MediaItemInfo( - /* windowDurationUs= */ 7000000L, - /* defaultStartPositionUs= */ 20L, - Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 0L, - /* isSeekable= */ true, - /* isDynamic= */ false)); - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - mediaItemInfos, - new DefaultShuffleOrder(5)); - - assertThat(timeline.getPeriodCount()).isEqualTo(6); - } - - @Test - public void getPeriod_forPopulatedPeriod_producesExpectedOutput() { - HashMap mediaItemInfos = new HashMap<>(); - MediaItemInfo.Period period1 = - new MediaItemInfo.Period( - "id1", /* durationUs= */ 4000000L, /* positionInWindowUs= */ 1000000L); - MediaItemInfo.Period period2 = - new MediaItemInfo.Period( - "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 6000000L); - mediaItemInfos.put( - asUUID(5), - new MediaItemInfo( - /* windowDurationUs= */ 7000000L, - /* defaultStartPositionUs= */ 20L, - Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 0L, - /* isSeekable= */ true, - /* isDynamic= */ false)); - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - mediaItemInfos, - new DefaultShuffleOrder(5)); - Timeline.Period period = - timeline.getPeriod(/* periodIndex= */ 5, new Timeline.Period(), /* setIds= */ true); - Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 5); - - assertThat(period.durationUs).isEqualTo(5000000L); - assertThat(period.windowIndex).isEqualTo(4); - assertThat(period.id).isEqualTo("id2"); - assertThat(period.uid).isEqualTo(periodUid); - } - - @Test - public void getPeriod_forEmptyPeriod_producesExpectedOutput() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(5)); - Timeline.Period period = timeline.getPeriod(2, new Timeline.Period(), /* setIds= */ true); - Object uid = timeline.getUidOfPeriod(/* periodIndex= */ 2); - - assertThat(period.durationUs).isEqualTo(C.TIME_UNSET); - assertThat(period.windowIndex).isEqualTo(2); - assertThat(period.id).isEqualTo(MediaItemInfo.EMPTY.periods.get(0).id); - assertThat(period.uid).isEqualTo(uid); - } - - @Test - public void getIndexOfPeriod_worksAcrossDifferentTimelines() { - MediaItemInfo.Period period1 = - new MediaItemInfo.Period( - "id1", /* durationUs= */ 4000000L, /* positionInWindowUs= */ 1000000L); - MediaItemInfo.Period period2 = - new MediaItemInfo.Period( - "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L); - - HashMap mediaItemInfos1 = new HashMap<>(); - mediaItemInfos1.put( - asUUID(1), - new MediaItemInfo( - /* windowDurationUs= */ 5000000L, - /* defaultStartPositionUs= */ 20L, - Collections.singletonList(period2), - /* positionInFirstPeriodUs= */ 0L, - /* isSeekable= */ true, - /* isDynamic= */ false)); - ExoCastTimeline timeline1 = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2), mediaItemInfos1, new DefaultShuffleOrder(2)); - - HashMap mediaItemInfos2 = new HashMap<>(); - mediaItemInfos2.put( - asUUID(1), - new MediaItemInfo( - /* windowDurationUs= */ 7000000L, - /* defaultStartPositionUs= */ 20L, - Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 0L, - /* isSeekable= */ true, - /* isDynamic= */ false)); - ExoCastTimeline timeline2 = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem2, mediaItem1, mediaItem3, mediaItem4, mediaItem5), - mediaItemInfos2, - new DefaultShuffleOrder(5)); - Object uidOfFirstPeriod = timeline1.getUidOfPeriod(0); - - assertThat(timeline1.getIndexOfPeriod(uidOfFirstPeriod)).isEqualTo(0); - assertThat(timeline2.getIndexOfPeriod(uidOfFirstPeriod)).isEqualTo(2); - } - - @Test - public void getIndexOfPeriod_forLastPeriod_producesExpectedOutput() { - MediaItemInfo.Period period1 = - new MediaItemInfo.Period( - "id1", /* durationUs= */ 4000000L, /* positionInWindowUs= */ 1000000L); - MediaItemInfo.Period period2 = - new MediaItemInfo.Period( - "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L); - - HashMap mediaItemInfos1 = new HashMap<>(); - mediaItemInfos1.put( - asUUID(5), - new MediaItemInfo( - /* windowDurationUs= */ 4000000L, - /* defaultStartPositionUs= */ 20L, - Collections.singletonList(period2), - /* positionInFirstPeriodUs= */ 0L, - /* isSeekable= */ true, - /* isDynamic= */ false)); - ExoCastTimeline singlePeriodTimeline = - ExoCastTimeline.createTimelineFor( - Collections.singletonList(mediaItem5), mediaItemInfos1, new DefaultShuffleOrder(1)); - Object periodUid = singlePeriodTimeline.getUidOfPeriod(0); - - HashMap mediaItemInfos2 = new HashMap<>(); - mediaItemInfos2.put( - asUUID(5), - new MediaItemInfo( - /* windowDurationUs= */ 7000000L, - /* defaultStartPositionUs= */ 20L, - Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 0L, - /* isSeekable= */ true, - /* isDynamic= */ false)); - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - mediaItemInfos2, - new DefaultShuffleOrder(5)); - - assertThat(timeline.getIndexOfPeriod(periodUid)).isEqualTo(5); - } - - @Test - public void getUidOfPeriod_withInvalidUid_returnsUnsetIndex() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(/* length= */ 5)); - - assertThat(timeline.getIndexOfPeriod(new Object())).isEqualTo(C.INDEX_UNSET); - } - - @Test - public void getFirstWindowIndex_returnsIndexAccordingToShuffleMode() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); - - assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(0); - assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(1); - } - - @Test - public void getLastWindowIndex_returnsIndexAccordingToShuffleMode() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); - - assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(4); - assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(3); - } - - @Test - public void getNextWindowIndex_repeatModeOne_returnsSameIndex() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(5)); - - for (int i = 0; i < 5; i++) { - assertThat( - timeline.getNextWindowIndex( - i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true)) - .isEqualTo(i); - assertThat( - timeline.getNextWindowIndex( - i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false)) - .isEqualTo(i); - } - } - - @Test - public void getNextWindowIndex_onLastIndex_returnsExpectedIndex() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); - - // Shuffle mode disabled: - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 4, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false)) - .isEqualTo(C.INDEX_UNSET); - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 4, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false)) - .isEqualTo(0); - // Shuffle mode enabled: - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 3, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) - .isEqualTo(C.INDEX_UNSET); - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 3, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true)) - .isEqualTo(1); - } - - @Test - public void getNextWindowIndex_inMiddleOfQueue_returnsNextIndex() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); - - // Shuffle mode disabled: - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 2, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false)) - .isEqualTo(3); - // Shuffle mode enabled: - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 2, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) - .isEqualTo(0); - } - - @Test - public void getPreviousWindowIndex_repeatModeOne_returnsSameIndex() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); - - for (int i = 0; i < 5; i++) { - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true)) - .isEqualTo(i); - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false)) - .isEqualTo(i); - } - } - - @Test - public void getPreviousWindowIndex_onFirstIndex_returnsExpectedIndex() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); - - // Shuffle mode disabled: - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ 0, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false)) - .isEqualTo(C.INDEX_UNSET); - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ 0, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false)) - .isEqualTo(4); - // Shuffle mode enabled: - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ 1, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) - .isEqualTo(C.INDEX_UNSET); - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ 1, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true)) - .isEqualTo(3); - } - - @Test - public void getPreviousWindowIndex_inMiddleOfQueue_returnsPreviousIndex() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); - - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ 4, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false)) - .isEqualTo(3); - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ 4, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) - .isEqualTo(0); - } - - private static UUID asUUID(long number) { - return new UUID(/* mostSigBits= */ 0L, number); - } -} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdateTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdateTest.java deleted file mode 100644 index fbe936a016..0000000000 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdateTest.java +++ /dev/null @@ -1,378 +0,0 @@ -/* - * 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.ext.cast; - -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DEFAULT_START_POSITION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISCONTINUITY_REASON; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DURATION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ERROR_MESSAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_DYNAMIC; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_LOADING; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_SEEKABLE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_ITEMS_INFO; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_QUEUE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIODS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIOD_ID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_PARAMETERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_POSITION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_STATE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_IN_FIRST_PERIOD_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TRACK_SELECTION_PARAMETERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_WINDOW_DURATION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_SEEK; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_FORCED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_BUFFERING; -import static com.google.common.truth.Truth.assertThat; - -import android.net.Uri; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.ShuffleOrder; -import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; -import com.google.android.exoplayer2.util.Util; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.UUID; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit tests for {@link ReceiverAppStateUpdate}. */ -@RunWith(AndroidJUnit4.class) -public class ReceiverAppStateUpdateTest { - - private static final long MOCK_SEQUENCE_NUMBER = 1; - - @Test - public void statusUpdate_withPlayWhenReady_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER).setPlayWhenReady(true).build(); - JSONObject stateMessage = createStateMessage().put(KEY_PLAY_WHEN_READY, true); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withPlaybackState_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) - .setPlaybackState(Player.STATE_BUFFERING) - .build(); - JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_STATE, STR_STATE_BUFFERING); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withMediaQueue_producesExpectedUpdate() throws JSONException { - HashMap requestHeaders = new HashMap<>(); - requestHeaders.put("key", "value"); - MediaItem.UriBundle media = new MediaItem.UriBundle(Uri.parse("www.media.com"), requestHeaders); - MediaItem.DrmScheme drmScheme1 = - new MediaItem.DrmScheme( - C.WIDEVINE_UUID, - new MediaItem.UriBundle(Uri.parse("www.widevine.com"), requestHeaders)); - MediaItem.DrmScheme drmScheme2 = - new MediaItem.DrmScheme( - C.PLAYREADY_UUID, - new MediaItem.UriBundle(Uri.parse("www.playready.com"), requestHeaders)); - MediaItem item = - new MediaItem.Builder() - .setTitle("title") - .setDescription("description") - .setMedia(media) - .setDrmSchemes(Arrays.asList(drmScheme1, drmScheme2)) - .setStartPositionUs(10) - .setEndPositionUs(20) - .build(); - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) - .setItems(Collections.singletonList(item)) - .build(); - JSONObject object = - createStateMessage() - .put(KEY_MEDIA_QUEUE, new JSONArray().put(ExoCastMessage.mediaItemAsJsonObject(item))); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(object.toString())).isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withRepeatMode_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) - .setRepeatMode(Player.REPEAT_MODE_OFF) - .build(); - JSONObject stateMessage = createStateMessage().put(KEY_REPEAT_MODE, STR_REPEAT_MODE_OFF); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withShuffleModeEnabled_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER).setShuffleModeEnabled(false).build(); - JSONObject stateMessage = createStateMessage().put(KEY_SHUFFLE_MODE_ENABLED, false); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withIsLoading_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER).setIsLoading(true).build(); - JSONObject stateMessage = createStateMessage().put(KEY_IS_LOADING, true); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withPlaybackParameters_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) - .setPlaybackParameters( - new PlaybackParameters( - /* speed= */ .5f, /* pitch= */ .25f, /* skipSilence= */ false)) - .build(); - JSONObject playbackParamsJson = - new JSONObject().put(KEY_SPEED, .5).put(KEY_PITCH, .25).put(KEY_SKIP_SILENCE, false); - JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_PARAMETERS, playbackParamsJson); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withTrackSelectionParameters_producesExpectedUpdate() - throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) - .setTrackSelectionParameters( - TrackSelectionParameters.DEFAULT - .buildUpon() - .setDisabledTextTrackSelectionFlags( - C.SELECTION_FLAG_FORCED | C.SELECTION_FLAG_DEFAULT) - .setPreferredAudioLanguage("esp") - .setPreferredTextLanguage("deu") - .setSelectUndeterminedTextLanguage(true) - .build()) - .build(); - - JSONArray selectionFlagsJson = - new JSONArray() - .put(ExoCastConstants.STR_SELECTION_FLAG_DEFAULT) - .put(STR_SELECTION_FLAG_FORCED); - JSONObject playbackParamsJson = - new JSONObject() - .put(KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS, selectionFlagsJson) - .put(KEY_PREFERRED_AUDIO_LANGUAGE, "esp") - .put(KEY_PREFERRED_TEXT_LANGUAGE, "deu") - .put(KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE, true); - JSONObject object = - createStateMessage().put(KEY_TRACK_SELECTION_PARAMETERS, playbackParamsJson); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(object.toString())).isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withError_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) - .setErrorMessage("error message") - .build(); - JSONObject stateMessage = createStateMessage().put(KEY_ERROR_MESSAGE, "error message"); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withPlaybackPosition_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) - .setPlaybackPosition( - new UUID(/* mostSigBits= */ 0, /* leastSigBits= */ 1), "period", 10L) - .build(); - JSONObject positionJson = - new JSONObject() - .put(KEY_UUID, new UUID(0, 1)) - .put(KEY_POSITION_MS, 10) - .put(KEY_PERIOD_ID, "period"); - JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_POSITION, positionJson); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withDiscontinuity_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) - .setPlaybackPosition( - new UUID(/* mostSigBits= */ 0, /* leastSigBits= */ 1), "period", 10L) - .setDiscontinuityReason(Player.DISCONTINUITY_REASON_SEEK) - .build(); - JSONObject positionJson = - new JSONObject() - .put(KEY_UUID, new UUID(0, 1)) - .put(KEY_POSITION_MS, 10) - .put(KEY_PERIOD_ID, "period") - .put(KEY_DISCONTINUITY_REASON, STR_DISCONTINUITY_REASON_SEEK); - JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_POSITION, positionJson); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withMediaItemInfo_producesExpectedTimeline() throws JSONException { - MediaItem.Builder builder = new MediaItem.Builder(); - MediaItem item1 = builder.setUuid(new UUID(0, 1)).build(); - MediaItem item2 = builder.setUuid(new UUID(0, 2)).build(); - - JSONArray periodsJson = new JSONArray(); - periodsJson - .put(new JSONObject().put(KEY_ID, "id1").put(KEY_DURATION_US, 5000000L)) - .put(new JSONObject().put(KEY_ID, "id2").put(KEY_DURATION_US, 7000000L)) - .put(new JSONObject().put(KEY_ID, "id3").put(KEY_DURATION_US, 6000000L)); - JSONObject mediaItemInfoForUuid1 = new JSONObject(); - mediaItemInfoForUuid1 - .put(KEY_WINDOW_DURATION_US, 10000000L) - .put(KEY_DEFAULT_START_POSITION_US, 1000000L) - .put(KEY_PERIODS, periodsJson) - .put(KEY_POSITION_IN_FIRST_PERIOD_US, 2000000L) - .put(KEY_IS_DYNAMIC, false) - .put(KEY_IS_SEEKABLE, true); - JSONObject mediaItemInfoMapJson = - new JSONObject().put(new UUID(0, 1).toString(), mediaItemInfoForUuid1); - - JSONObject receiverAppStateUpdateJson = - createStateMessage().put(KEY_MEDIA_ITEMS_INFO, mediaItemInfoMapJson); - ReceiverAppStateUpdate receiverAppStateUpdate = - ReceiverAppStateUpdate.fromJsonMessage(receiverAppStateUpdateJson.toString()); - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(item1, item2), - receiverAppStateUpdate.mediaItemsInformation, - new ShuffleOrder.DefaultShuffleOrder( - /* shuffledIndices= */ new int[] {1, 0}, /* randomSeed= */ 0)); - Timeline.Window window0 = - timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window(), /* setTag= */ true); - Timeline.Window window1 = - timeline.getWindow(/* windowIndex= */ 1, new Timeline.Window(), /* setTag= */ true); - Timeline.Period[] periods = new Timeline.Period[4]; - for (int i = 0; i < 4; i++) { - periods[i] = - timeline.getPeriod(/* periodIndex= */ i, new Timeline.Period(), /* setIds= */ true); - } - - assertThat(timeline.getWindowCount()).isEqualTo(2); - assertThat(window0.positionInFirstPeriodUs).isEqualTo(2000000L); - assertThat(window0.durationUs).isEqualTo(10000000L); - assertThat(window0.isDynamic).isFalse(); - assertThat(window0.isSeekable).isTrue(); - assertThat(window0.defaultPositionUs).isEqualTo(1000000L); - assertThat(window1.positionInFirstPeriodUs).isEqualTo(0L); - assertThat(window1.durationUs).isEqualTo(C.TIME_UNSET); - assertThat(window1.isDynamic).isTrue(); - assertThat(window1.isSeekable).isFalse(); - assertThat(window1.defaultPositionUs).isEqualTo(0L); - - assertThat(timeline.getPeriodCount()).isEqualTo(4); - assertThat(periods[0].id).isEqualTo("id1"); - assertThat(periods[0].getPositionInWindowUs()).isEqualTo(-2000000L); - assertThat(periods[0].durationUs).isEqualTo(5000000L); - assertThat(periods[1].id).isEqualTo("id2"); - assertThat(periods[1].durationUs).isEqualTo(7000000L); - assertThat(periods[1].getPositionInWindowUs()).isEqualTo(3000000L); - assertThat(periods[2].id).isEqualTo("id3"); - assertThat(periods[2].durationUs).isEqualTo(6000000L); - assertThat(periods[2].getPositionInWindowUs()).isEqualTo(10000000L); - assertThat(periods[3].durationUs).isEqualTo(C.TIME_UNSET); - } - - @Test - public void statusUpdate_withShuffleOrder_producesExpectedTimeline() throws JSONException { - MediaItem.Builder builder = new MediaItem.Builder(); - JSONObject receiverAppStateUpdateJson = - createStateMessage().put(KEY_SHUFFLE_ORDER, new JSONArray(Arrays.asList(2, 3, 1, 0))); - ReceiverAppStateUpdate receiverAppStateUpdate = - ReceiverAppStateUpdate.fromJsonMessage(receiverAppStateUpdateJson.toString()); - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - /* mediaItems= */ Arrays.asList( - builder.build(), builder.build(), builder.build(), builder.build()), - /* mediaItemInfoMap= */ Collections.emptyMap(), - /* shuffleOrder= */ new ShuffleOrder.DefaultShuffleOrder( - Util.toArray(receiverAppStateUpdate.shuffleOrder), /* randomSeed= */ 0)); - - assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(2); - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 2, - /* repeatMode= */ Player.REPEAT_MODE_OFF, - /* shuffleModeEnabled= */ true)) - .isEqualTo(3); - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 3, - /* repeatMode= */ Player.REPEAT_MODE_OFF, - /* shuffleModeEnabled= */ true)) - .isEqualTo(1); - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 1, - /* repeatMode= */ Player.REPEAT_MODE_OFF, - /* shuffleModeEnabled= */ true)) - .isEqualTo(0); - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 0, - /* repeatMode= */ Player.REPEAT_MODE_OFF, - /* shuffleModeEnabled= */ true)) - .isEqualTo(C.INDEX_UNSET); - } - - private static JSONObject createStateMessage() throws JSONException { - return new JSONObject().put(KEY_SEQUENCE_NUMBER, MOCK_SEQUENCE_NUMBER); - } -} From 259bea1652dc44cff0f06d0d865707c12096d4de Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 1 Jul 2019 19:13:03 +0100 Subject: [PATCH 1407/1556] MediaSessionConnector: Document how to provide metadata asynchronously Issue: #6047 PiperOrigin-RevId: 255992898 --- .../exoplayer2/ext/mediasession/MediaSessionConnector.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 eaebf8b4e1..3136e3cca9 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 @@ -378,6 +378,13 @@ public final class MediaSessionConnector { /** * Gets the {@link MediaMetadataCompat} to be published to the session. * + *

        An app may need to load metadata resources like artwork bitmaps asynchronously. In such a + * case the app should return a {@link MediaMetadataCompat} object that does not contain these + * resources as a placeholder. The app should start an asynchronous operation to download the + * bitmap and put it into a cache. Finally, the app should call {@link + * #invalidateMediaSessionMetadata()}. This causes this callback to be called again and the app + * can now return a {@link MediaMetadataCompat} object with all the resources included. + * * @param player The player connected to the media session. * @return The {@link MediaMetadataCompat} to be published to the session. */ From 6febc88dce52501d40d5dda2c83289584f8d5a09 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jul 2019 13:42:05 +0100 Subject: [PATCH 1408/1556] FLV extractor fixes 1. Only output video starting from a keyframe 2. When calculating the timestamp offset to adjust live streams to start at t=0, use the timestamp of the first tag from which a sample is actually output, rather than just the first audio/video tag. The test streams in the referenced GitHub issue start with a video tag whose packet type is AVC_PACKET_TYPE_SEQUENCE_HEADER (i.e. does not contain a sample) and whose timestamp is set to 0 (i.e. isn't set). The timestamp is set correctly on tags that from which a sample is actually output. Issue: #6111 PiperOrigin-RevId: 256147747 --- RELEASENOTES.md | 2 ++ .../extractor/flv/AudioTagPayloadReader.java | 8 ++++-- .../extractor/flv/FlvExtractor.java | 26 ++++++++++++------- .../extractor/flv/ScriptTagPayloadReader.java | 7 ++--- .../extractor/flv/TagPayloadReader.java | 16 ++++++------ .../extractor/flv/VideoTagPayloadReader.java | 18 ++++++++++--- 6 files changed, 51 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 62e985f98b..d76ca54b7b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,8 @@ * Wrap decoder exceptions in a new `DecoderException` class and report as renderer error. * SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. +* FLV: Fix bug that caused playback of some live streams to not start + ([#6111](https://github.com/google/ExoPlayer/issues/6111)). ### 2.10.2 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java index ec5ad88aeb..b10f2bf80b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java @@ -86,11 +86,12 @@ import java.util.Collections; } @Override - protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { + protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException { if (audioFormat == AUDIO_FORMAT_MP3) { int sampleSize = data.bytesLeft(); output.sampleData(data, sampleSize); output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + return true; } else { int packetType = data.readUnsignedByte(); if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { @@ -104,12 +105,15 @@ import java.util.Collections; Collections.singletonList(audioSpecificConfig), null, 0, null); output.format(format); hasOutputFormat = true; + return false; } else if (audioFormat != AUDIO_FORMAT_AAC || packetType == AAC_PACKET_TYPE_AAC_RAW) { int sampleSize = data.bytesLeft(); output.sampleData(data, sampleSize); output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + return true; + } else { + return false; } } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index 15b36157fb..f6835558f2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -73,6 +73,7 @@ public final class FlvExtractor implements Extractor { private ExtractorOutput extractorOutput; private @States int state; + private boolean outputFirstSample; private long mediaTagTimestampOffsetUs; private int bytesToNextTagHeader; private int tagType; @@ -89,7 +90,6 @@ public final class FlvExtractor implements Extractor { tagData = new ParsableByteArray(); metadataReader = new ScriptTagPayloadReader(); state = STATE_READING_FLV_HEADER; - mediaTagTimestampOffsetUs = C.TIME_UNSET; } @Override @@ -131,7 +131,7 @@ public final class FlvExtractor implements Extractor { @Override public void seek(long position, long timeUs) { state = STATE_READING_FLV_HEADER; - mediaTagTimestampOffsetUs = C.TIME_UNSET; + outputFirstSample = false; bytesToNextTagHeader = 0; } @@ -252,14 +252,16 @@ public final class FlvExtractor implements Extractor { */ private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { boolean wasConsumed = true; + boolean wasSampleOutput = false; + long timestampUs = getCurrentTimestampUs(); if (tagType == TAG_TYPE_AUDIO && audioReader != null) { ensureReadyForMediaOutput(); - audioReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs); + wasSampleOutput = audioReader.consume(prepareTagData(input), timestampUs); } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) { ensureReadyForMediaOutput(); - videoReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs); + wasSampleOutput = videoReader.consume(prepareTagData(input), timestampUs); } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) { - metadataReader.consume(prepareTagData(input), tagTimestampUs); + wasSampleOutput = metadataReader.consume(prepareTagData(input), timestampUs); long durationUs = metadataReader.getDurationUs(); if (durationUs != C.TIME_UNSET) { extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); @@ -269,6 +271,11 @@ public final class FlvExtractor implements Extractor { input.skipFully(tagDataSize); wasConsumed = false; } + if (!outputFirstSample && wasSampleOutput) { + outputFirstSample = true; + mediaTagTimestampOffsetUs = + metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0; + } bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header. state = STATE_SKIPPING_TO_TAG_HEADER; return wasConsumed; @@ -291,10 +298,11 @@ public final class FlvExtractor implements Extractor { extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); outputSeekMap = true; } - if (mediaTagTimestampOffsetUs == C.TIME_UNSET) { - mediaTagTimestampOffsetUs = - metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0; - } } + private long getCurrentTimestampUs() { + return outputFirstSample + ? (mediaTagTimestampOffsetUs + tagTimestampUs) + : (metadataReader.getDurationUs() == C.TIME_UNSET ? 0 : tagTimestampUs); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java index 2dec85ffcc..eb1cc8f336 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java @@ -63,7 +63,7 @@ import java.util.Map; } @Override - protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { + protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException { int nameType = readAmfType(data); if (nameType != AMF_TYPE_STRING) { // Should never happen. @@ -72,12 +72,12 @@ import java.util.Map; String name = readAmfString(data); if (!NAME_METADATA.equals(name)) { // We're only interested in metadata. - return; + return false; } int type = readAmfType(data); if (type != AMF_TYPE_ECMA_ARRAY) { // We're not interested in this metadata. - return; + return false; } // Set the duration to the value contained in the metadata, if present. Map metadata = readAmfEcmaArray(data); @@ -87,6 +87,7 @@ import java.util.Map; durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND); } } + return false; } private static int readAmfType(ParsableByteArray data) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java index e8652d653f..48914b7c2c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java @@ -58,12 +58,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray; * * @param data The payload data to consume. * @param timeUs The timestamp associated with the payload. + * @return Whether a sample was output. * @throws ParserException If an error occurs parsing the data. */ - public final void consume(ParsableByteArray data, long timeUs) throws ParserException { - if (parseHeader(data)) { - parsePayload(data, timeUs); - } + public final boolean consume(ParsableByteArray data, long timeUs) throws ParserException { + return parseHeader(data) && parsePayload(data, timeUs); } /** @@ -78,10 +77,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray; /** * Parses tag payload. * - * @param data Buffer where tag payload is stored - * @param timeUs Time position of the frame + * @param data Buffer where tag payload is stored. + * @param timeUs Time position of the frame. + * @return Whether a sample was output. * @throws ParserException If an error occurs parsing the payload. */ - protected abstract void parsePayload(ParsableByteArray data, long timeUs) throws ParserException; - + protected abstract boolean parsePayload(ParsableByteArray data, long timeUs) + throws ParserException; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java index 92db91e20b..5ddaafb4a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java @@ -47,6 +47,7 @@ import com.google.android.exoplayer2.video.AvcConfig; // State variables. private boolean hasOutputFormat; + private boolean hasOutputKeyframe; private int frameType; /** @@ -60,7 +61,7 @@ import com.google.android.exoplayer2.video.AvcConfig; @Override public void seek() { - // Do nothing. + hasOutputKeyframe = false; } @Override @@ -77,7 +78,7 @@ import com.google.android.exoplayer2.video.AvcConfig; } @Override - protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { + protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException { int packetType = data.readUnsignedByte(); int compositionTimeMs = data.readInt24(); @@ -94,7 +95,12 @@ import com.google.android.exoplayer2.video.AvcConfig; avcConfig.initializationData, Format.NO_VALUE, avcConfig.pixelWidthAspectRatio, null); output.format(format); hasOutputFormat = true; + return false; } else if (packetType == AVC_PACKET_TYPE_AVC_NALU && hasOutputFormat) { + boolean isKeyframe = frameType == VIDEO_FRAME_KEYFRAME; + if (!hasOutputKeyframe && !isKeyframe) { + return false; + } // TODO: Deduplicate with Mp4Extractor. // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case // they're only 1 or 2 bytes long. @@ -123,8 +129,12 @@ import com.google.android.exoplayer2.video.AvcConfig; output.sampleData(data, bytesToWrite); bytesWritten += bytesToWrite; } - output.sampleMetadata(timeUs, frameType == VIDEO_FRAME_KEYFRAME ? C.BUFFER_FLAG_KEY_FRAME : 0, - bytesWritten, 0, null); + output.sampleMetadata( + timeUs, isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0, bytesWritten, 0, null); + hasOutputKeyframe = true; + return true; + } else { + return false; } } From 7408b4355a990f9a450815ddea9c62945ea9543a Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 2 Jul 2019 15:33:50 +0100 Subject: [PATCH 1409/1556] Add DRM format support checks for MediaSource provided DRM PiperOrigin-RevId: 256161522 --- .../ext/opus/LibopusAudioRenderer.java | 8 +- .../exoplayer2/ext/opus/OpusLibrary.java | 18 ++- .../ext/vp9/LibvpxVideoRenderer.java | 8 +- .../exoplayer2/ext/vp9/VpxLibrary.java | 18 ++- .../com/google/android/exoplayer2/Format.java | 118 ++++++++++++++---- .../audio/MediaCodecAudioRenderer.java | 6 +- .../drm/DefaultDrmSessionManager.java | 6 + .../exoplayer2/drm/DrmSessionManager.java | 14 +++ .../android/exoplayer2/drm/ExoMediaDrm.java | 3 + .../exoplayer2/drm/FrameworkMediaDrm.java | 5 + .../video/MediaCodecVideoRenderer.java | 7 +- .../google/android/exoplayer2/FormatTest.java | 3 +- 12 files changed, 184 insertions(+), 30 deletions(-) diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index 59337c0847..c58293dd45 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -77,12 +77,17 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { @Override protected int supportsFormatInternal(DrmSessionManager drmSessionManager, Format format) { + boolean drmIsSupported = + format.drmInitData == null + || OpusLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType) + || (format.exoMediaCryptoType == null + && supportsFormatDrm(drmSessionManager, format.drmInitData)); if (!OpusLibrary.isAvailable() || !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; } else if (!supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) { return FORMAT_UNSUPPORTED_SUBTYPE; - } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { + } else if (!drmIsSupported) { return FORMAT_UNSUPPORTED_DRM; } else { return FORMAT_HANDLED; @@ -110,5 +115,4 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { Format.NO_VALUE, decoder.getChannelCount(), decoder.getSampleRate(), C.ENCODING_PCM_16BIT, null, null, 0, null); } - } diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java index 285be96388..cd5d67f686 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java @@ -16,7 +16,9 @@ package com.google.android.exoplayer2.ext.opus; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.LibraryLoader; +import com.google.android.exoplayer2.util.Util; /** * Configures and queries the underlying native library. @@ -28,6 +30,7 @@ public final class OpusLibrary { } private static final LibraryLoader LOADER = new LibraryLoader("opusV2JNI"); + private static Class exoMediaCryptoType; private OpusLibrary() {} @@ -36,10 +39,14 @@ public final class OpusLibrary { * it must do so before calling any other method defined by this class, and before instantiating a * {@link LibopusAudioRenderer} instance. * + * @param exoMediaCryptoType The {@link ExoMediaCrypto} type expected for decoding protected + * content. * @param libraries The names of the Opus native libraries. */ - public static void setLibraries(String... libraries) { + public static void setLibraries( + Class exoMediaCryptoType, String... libraries) { LOADER.setLibraries(libraries); + OpusLibrary.exoMediaCryptoType = exoMediaCryptoType; } /** @@ -56,6 +63,15 @@ public final class OpusLibrary { return isAvailable() ? opusGetVersion() : null; } + /** + * Returns whether the given {@link ExoMediaCrypto} type matches the one required for decoding + * protected content. + */ + public static boolean matchesExpectedExoMediaCryptoType( + Class exoMediaCryptoType) { + return Util.areEqual(OpusLibrary.exoMediaCryptoType, exoMediaCryptoType); + } + public static native String opusGetVersion(); public static native boolean opusIsSecureDecodeSupported(); } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index 1734a05e86..b4db4971cc 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -284,7 +284,13 @@ public class LibvpxVideoRenderer extends BaseRenderer { public int supportsFormat(Format format) { if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; - } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { + } + boolean drmIsSupported = + format.drmInitData == null + || VpxLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType) + || (format.exoMediaCryptoType == null + && supportsFormatDrm(drmSessionManager, format.drmInitData)); + if (!drmIsSupported) { return FORMAT_UNSUPPORTED_DRM; } return FORMAT_HANDLED | ADAPTIVE_SEAMLESS; diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java index 5a65fc56ff..2c25f674d6 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java @@ -16,7 +16,9 @@ package com.google.android.exoplayer2.ext.vp9; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.LibraryLoader; +import com.google.android.exoplayer2.util.Util; /** * Configures and queries the underlying native library. @@ -28,6 +30,7 @@ public final class VpxLibrary { } private static final LibraryLoader LOADER = new LibraryLoader("vpx", "vpxV2JNI"); + private static Class exoMediaCryptoType; private VpxLibrary() {} @@ -36,10 +39,14 @@ public final class VpxLibrary { * it must do so before calling any other method defined by this class, and before instantiating a * {@link LibvpxVideoRenderer} instance. * + * @param exoMediaCryptoType The {@link ExoMediaCrypto} type required for decoding protected + * content. * @param libraries The names of the Vpx native libraries. */ - public static void setLibraries(String... libraries) { + public static void setLibraries( + Class exoMediaCryptoType, String... libraries) { LOADER.setLibraries(libraries); + VpxLibrary.exoMediaCryptoType = exoMediaCryptoType; } /** @@ -74,6 +81,15 @@ public final class VpxLibrary { return indexHbd >= 0; } + /** + * Returns whether the given {@link ExoMediaCrypto} type matches the one required for decoding + * protected content. + */ + public static boolean matchesExpectedExoMediaCryptoType( + Class exoMediaCryptoType) { + return Util.areEqual(VpxLibrary.exoMediaCryptoType, exoMediaCryptoType); + } + private static native String vpxGetVersion(); private static native String vpxGetBuildConfig(); public static native boolean vpxIsSecureDecodeSupported(); 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 a482022a17..f06c9da048 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 @@ -19,6 +19,8 @@ import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.Nullable; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -163,6 +165,15 @@ public final class Format implements Parcelable { */ public final int accessibilityChannel; + // Provided by source. + + /** + * The type of the {@link ExoMediaCrypto} provided by the media source, if the media source can + * acquire a {@link DrmSession} for {@link #drmInitData}. Null if the media source cannot acquire + * a session for {@link #drmInitData}, or if not applicable. + */ + @Nullable public final Class exoMediaCryptoType; + // Lazily initialized hashcode. private int hashCode; @@ -236,7 +247,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, /* language= */ null, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } public static Format createVideoSampleFormat( @@ -340,7 +352,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, /* language= */ null, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Audio. @@ -413,7 +426,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } public static Format createAudioSampleFormat( @@ -518,7 +532,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Text. @@ -585,7 +600,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - accessibilityChannel); + accessibilityChannel, + /* exoMediaCryptoType= */ null); } public static Format createTextSampleFormat( @@ -698,7 +714,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - accessibilityChannel); + accessibilityChannel, + /* exoMediaCryptoType= */ null); } // Image. @@ -740,7 +757,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Generic. @@ -804,7 +822,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } public static Format createSampleFormat( @@ -837,7 +856,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, /* language= */ null, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } public static Format createSampleFormat( @@ -874,7 +894,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, /* language= */ null, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } /* package */ Format( @@ -910,7 +931,9 @@ public final class Format implements Parcelable { int encoderPadding, // Audio and text specific. @Nullable String language, - int accessibilityChannel) { + int accessibilityChannel, + // Provided by source. + @Nullable Class exoMediaCryptoType) { this.id = id; this.label = label; this.selectionFlags = selectionFlags; @@ -946,6 +969,8 @@ public final class Format implements Parcelable { // Audio and text specific. this.language = Util.normalizeLanguageCode(language); this.accessibilityChannel = accessibilityChannel; + // Provided by source. + this.exoMediaCryptoType = exoMediaCryptoType; } @SuppressWarnings("ResourceType") @@ -988,6 +1013,8 @@ public final class Format implements Parcelable { // Audio and text specific. language = in.readString(); accessibilityChannel = in.readInt(); + // Provided by source. + exoMediaCryptoType = null; } public Format copyWithMaxInputSize(int maxInputSize) { @@ -1019,7 +1046,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { @@ -1051,7 +1079,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithContainerInfo( @@ -1099,7 +1128,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } @SuppressWarnings("ReferenceEquality") @@ -1178,7 +1208,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { @@ -1210,7 +1241,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithFrameRate(float frameRate) { @@ -1242,7 +1274,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) { @@ -1274,7 +1307,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithMetadata(@Nullable Metadata metadata) { @@ -1306,7 +1340,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithRotationDegrees(int rotationDegrees) { @@ -1338,7 +1373,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithBitrate(int bitrate) { @@ -1370,7 +1406,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithVideoSize(int width, int height) { @@ -1402,7 +1439,41 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); + } + + public Format copyWithExoMediaCryptoType(Class exoMediaCryptoType) { + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel, + exoMediaCryptoType); } /** @@ -1481,6 +1552,8 @@ public final class Format implements Parcelable { // Audio and text specific. result = 31 * result + (language == null ? 0 : language.hashCode()); result = 31 * result + accessibilityChannel; + // Provided by source. + result = 31 * result + (exoMediaCryptoType == null ? 0 : exoMediaCryptoType.hashCode()); hashCode = result; } return hashCode; @@ -1516,6 +1589,7 @@ public final class Format implements Parcelable { && accessibilityChannel == other.accessibilityChannel && Float.compare(frameRate, other.frameRate) == 0 && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0 + && Util.areEqual(exoMediaCryptoType, other.exoMediaCryptoType) && Util.areEqual(id, other.id) && Util.areEqual(label, other.label) && Util.areEqual(codecs, other.codecs) 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 17591a585e..7e889097bc 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 @@ -308,7 +308,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return FORMAT_UNSUPPORTED_TYPE; } int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; - boolean supportsFormatDrm = supportsFormatDrm(drmSessionManager, format.drmInitData); + boolean supportsFormatDrm = + format.drmInitData == null + || FrameworkMediaCrypto.class.equals(format.exoMediaCryptoType) + || (format.exoMediaCryptoType == null + && supportsFormatDrm(drmSessionManager, format.drmInitData)); if (supportsFormatDrm && allowPassthrough(format.channelCount, mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) { 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 4e18df04e3..34fd223c64 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 @@ -436,6 +436,12 @@ public class DefaultDrmSessionManager return session; } + @Override + @Nullable + public Class getExoMediaCryptoType(DrmInitData drmInitData) { + return canAcquireSession(drmInitData) ? mediaDrm.getExoMediaCryptoType() : null; + } + // ProvisioningManager implementation. @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java index 716da7a0ad..9211cec144 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.drm; import android.os.Looper; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -49,6 +50,12 @@ public interface DrmSessionManager { new DrmSession.DrmSessionException( new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME))); } + + @Override + @Nullable + public Class getExoMediaCryptoType(DrmInitData drmInitData) { + return null; + } }; /** Flags that control the handling of DRM protected content. */ @@ -99,4 +106,11 @@ public interface DrmSessionManager { default int getFlags() { return 0; } + + /** + * Returns the {@link ExoMediaCrypto} type returned by sessions acquired using the given {@link + * DrmInitData}, or null if a session cannot be acquired with the given {@link DrmInitData}. + */ + @Nullable + Class getExoMediaCryptoType(DrmInitData drmInitData); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java index 6bd8d9688f..ca776267aa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java @@ -271,4 +271,7 @@ public interface ExoMediaDrm { * @throws MediaCryptoException If the instance can't be created. */ T createMediaCrypto(byte[] sessionId) throws MediaCryptoException; + + /** Returns the {@link ExoMediaCrypto} type created by {@link #createMediaCrypto(byte[])}. */ + Class getExoMediaCryptoType(); } 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 609abd4e1e..e77504c91c 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 @@ -225,6 +225,11 @@ public final class FrameworkMediaDrm implements ExoMediaDrm getExoMediaCryptoType() { + return FrameworkMediaCrypto.class; + } + private static SchemeData getSchemeData(UUID uuid, List schemeDatas) { if (!C.WIDEVINE_UUID.equals(uuid)) { // For non-Widevine CDMs always use the first scheme data. 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 c864adfa68..d9d81cf6d4 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 @@ -336,7 +336,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (decoderInfos.isEmpty()) { return FORMAT_UNSUPPORTED_SUBTYPE; } - if (!supportsFormatDrm(drmSessionManager, drmInitData)) { + boolean supportsFormatDrm = + format.drmInitData == null + || FrameworkMediaCrypto.class.equals(format.exoMediaCryptoType) + || (format.exoMediaCryptoType == null + && supportsFormatDrm(drmSessionManager, format.drmInitData)); + if (!supportsFormatDrm) { return FORMAT_UNSUPPORTED_DRM; } // Check capabilities for the first decoder in the list, which takes priority. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java b/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java index 96bb606eae..fe2a8c7d4b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java @@ -91,7 +91,8 @@ public final class FormatTest { /* encoderDelay= */ 1001, /* encoderPadding= */ 1002, "language", - /* accessibilityChannel= */ Format.NO_VALUE); + /* accessibilityChannel= */ Format.NO_VALUE, + /* exoMediaCryptoType= */ null); Parcel parcel = Parcel.obtain(); formatToParcel.writeToParcel(parcel, 0); From 7964e51e0e46bd89f81c9f4b7ad75221ea24d8a4 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jul 2019 19:15:08 +0100 Subject: [PATCH 1410/1556] Remove more classes from nullness blacklist PiperOrigin-RevId: 256202135 --- .../ext/cast/DefaultCastOptionsProvider.java | 3 +- .../exoplayer2/ext/flac/FlacDecoder.java | 2 + .../exoplayer2/ext/flac/FlacDecoderJni.java | 42 +++++++++++-------- extensions/opus/build.gradle | 1 + .../exoplayer2/ext/opus/OpusDecoder.java | 2 + .../exoplayer2/ext/opus/OpusLibrary.java | 8 ++-- .../exoplayer2/ext/vp9/VpxDecoder.java | 6 ++- .../exoplayer2/ext/vp9/VpxLibrary.java | 9 ++-- .../exoplayer2/decoder/SimpleDecoder.java | 3 +- 9 files changed, 47 insertions(+), 29 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java index 8948173f60..ebadb0a08a 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java @@ -20,6 +20,7 @@ import com.google.android.gms.cast.CastMediaControlIntent; import com.google.android.gms.cast.framework.CastOptions; import com.google.android.gms.cast.framework.OptionsProvider; import com.google.android.gms.cast.framework.SessionProvider; +import java.util.Collections; import java.util.List; /** @@ -58,7 +59,7 @@ public final class DefaultCastOptionsProvider implements OptionsProvider { @Override public List getAdditionalSessionProviders(Context context) { - return null; + return Collections.emptyList(); } } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java index 2d74bce5f1..9b15aff846 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.flac; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; @@ -94,6 +95,7 @@ import java.util.List; } @Override + @Nullable protected FlacDecoderException decode( DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { if (reset) { diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java index de038921aa..a97d99fa54 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java @@ -15,9 +15,11 @@ */ package com.google.android.exoplayer2.ext.flac; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.nio.ByteBuffer; @@ -37,15 +39,16 @@ import java.nio.ByteBuffer; } } - private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has + private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size as libflac. private final long nativeDecoderContext; - private ByteBuffer byteBufferData; - private ExtractorInput extractorInput; + @Nullable private ByteBuffer byteBufferData; + @Nullable private ExtractorInput extractorInput; + @Nullable private byte[] tempBuffer; private boolean endOfExtractorInput; - private byte[] tempBuffer; + @SuppressWarnings("nullness:method.invocation.invalid") public FlacDecoderJni() throws FlacDecoderException { if (!FlacLibrary.isAvailable()) { throw new FlacDecoderException("Failed to load decoder native libraries."); @@ -58,7 +61,8 @@ import java.nio.ByteBuffer; /** * Sets data to be parsed by libflac. - * @param byteBufferData Source {@link ByteBuffer} + * + * @param byteBufferData Source {@link ByteBuffer}. */ public void setData(ByteBuffer byteBufferData) { this.byteBufferData = byteBufferData; @@ -68,7 +72,8 @@ import java.nio.ByteBuffer; /** * Sets data to be parsed by libflac. - * @param extractorInput Source {@link ExtractorInput} + * + * @param extractorInput Source {@link ExtractorInput}. */ public void setData(ExtractorInput extractorInput) { this.byteBufferData = null; @@ -90,15 +95,15 @@ import java.nio.ByteBuffer; /** * Reads up to {@code length} bytes from the data source. - *

        - * This method blocks until at least one byte of data can be read, the end of the input is + * + *

        This method blocks until at least one byte of data can be read, the end of the input is * detected or an exception is thrown. - *

        - * This method is called from the native code. + * + *

        This method is called from the native code. * * @param target A target {@link ByteBuffer} into which data should be written. - * @return Returns the number of bytes read, or -1 on failure. It's not an error if this returns - * zero; it just means all the data read from the source. + * @return Returns the number of bytes read, or -1 on failure. If all of the data has already been + * read from the source, then 0 is returned. */ public int read(ByteBuffer target) throws IOException, InterruptedException { int byteCount = target.remaining(); @@ -106,18 +111,20 @@ import java.nio.ByteBuffer; byteCount = Math.min(byteCount, byteBufferData.remaining()); int originalLimit = byteBufferData.limit(); byteBufferData.limit(byteBufferData.position() + byteCount); - target.put(byteBufferData); - byteBufferData.limit(originalLimit); } else if (extractorInput != null) { + ExtractorInput extractorInput = this.extractorInput; + byte[] tempBuffer = Util.castNonNull(this.tempBuffer); byteCount = Math.min(byteCount, TEMP_BUFFER_SIZE); - int read = readFromExtractorInput(0, byteCount); + int read = readFromExtractorInput(extractorInput, tempBuffer, /* offset= */ 0, byteCount); if (read < 4) { // Reading less than 4 bytes, most of the time, happens because of getting the bytes left in // the buffer of the input. Do another read to reduce the number of calls to this method // from the native code. - read += readFromExtractorInput(read, byteCount - read); + read += + readFromExtractorInput( + extractorInput, tempBuffer, read, /* length= */ byteCount - read); } byteCount = read; target.put(tempBuffer, 0, byteCount); @@ -234,7 +241,8 @@ import java.nio.ByteBuffer; flacRelease(nativeDecoderContext); } - private int readFromExtractorInput(int offset, int length) + private int readFromExtractorInput( + ExtractorInput extractorInput, byte[] tempBuffer, int offset, int length) throws IOException, InterruptedException { int read = extractorInput.read(tempBuffer, offset, length); if (read == C.RESULT_END_OF_INPUT) { diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index 56acbdb7d3..0795079c6b 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -39,6 +39,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') + implementation 'androidx.annotation:annotation:1.0.2' testImplementation project(modulePrefix + 'testutils-robolectric') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java index f8ec477b88..dbce33b923 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.opus; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; @@ -150,6 +151,7 @@ import java.util.List; } @Override + @Nullable protected OpusDecoderException decode( DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { if (reset) { diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java index cd5d67f686..5529701c06 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.opus; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.LibraryLoader; @@ -30,7 +31,7 @@ public final class OpusLibrary { } private static final LibraryLoader LOADER = new LibraryLoader("opusV2JNI"); - private static Class exoMediaCryptoType; + @Nullable private static Class exoMediaCryptoType; private OpusLibrary() {} @@ -56,9 +57,8 @@ public final class OpusLibrary { return LOADER.isAvailable(); } - /** - * Returns the version of the underlying library if available, or null otherwise. - */ + /** Returns the version of the underlying library if available, or null otherwise. */ + @Nullable public static String getVersion() { return isAvailable() ? opusGetVersion() : null; } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 57e5481b55..0e13e82630 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.vp9; +import androidx.annotation.Nullable; import android.view.Surface; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.CryptoInfo; @@ -120,8 +121,9 @@ import java.nio.ByteBuffer; } @Override - protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, - boolean reset) { + @Nullable + protected VpxDecoderException decode( + VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) { ByteBuffer inputData = inputBuffer.data; int inputSize = inputData.limit(); CryptoInfo cryptoInfo = inputBuffer.cryptoInfo; diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java index 2c25f674d6..5106ab67ad 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.vp9; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.LibraryLoader; @@ -30,7 +31,7 @@ public final class VpxLibrary { } private static final LibraryLoader LOADER = new LibraryLoader("vpx", "vpxV2JNI"); - private static Class exoMediaCryptoType; + @Nullable private static Class exoMediaCryptoType; private VpxLibrary() {} @@ -56,9 +57,8 @@ public final class VpxLibrary { return LOADER.isAvailable(); } - /** - * Returns the version of the underlying library if available, or null otherwise. - */ + /** Returns the version of the underlying library if available, or null otherwise. */ + @Nullable public static String getVersion() { return isAvailable() ? vpxGetVersion() : null; } @@ -67,6 +67,7 @@ public final class VpxLibrary { * Returns the configuration string with which the underlying library was built if available, or * null otherwise. */ + @Nullable public static String getBuildConfig() { return isAvailable() ? vpxGetBuildConfig() : null; } 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 f8204f6be3..b5650860e9 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 @@ -301,5 +301,6 @@ public abstract class SimpleDecoder< * @param reset Whether the decoder must be reset before decoding. * @return A decoder exception if an error occurred, or null if decoding was successful. */ - protected abstract @Nullable E decode(I inputBuffer, O outputBuffer, boolean reset); + @Nullable + protected abstract E decode(I inputBuffer, O outputBuffer, boolean reset); } From 4c2f211e28b33bd2b60aeb7cd25f8c01f78e4401 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jul 2019 20:14:38 +0100 Subject: [PATCH 1411/1556] Remove Flac and Opus renderers from nullness blacklist PiperOrigin-RevId: 256213895 --- .../ext/flac/LibflacAudioRenderer.java | 7 ++-- .../ext/opus/LibopusAudioRenderer.java | 36 ++++++++++++++----- .../exoplayer2/ext/opus/OpusLibrary.java | 2 +- .../exoplayer2/ext/vp9/VpxLibrary.java | 2 +- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java index ac7646cc4b..376d0fd75e 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.flac; import android.os.Handler; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; @@ -33,7 +34,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { private static final int NUM_BUFFERS = 16; public LibflacAudioRenderer() { - this(null, null); + this(/* eventHandler= */ null, /* eventListener= */ null); } /** @@ -43,8 +44,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public LibflacAudioRenderer( - Handler eventHandler, - AudioRendererEventListener eventListener, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) { super(eventHandler, eventListener, audioProcessors); } diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index c58293dd45..fe74f5c59c 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.opus; import android.os.Handler; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; @@ -35,10 +36,12 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { /** The default input buffer size. */ private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6; - private OpusDecoder decoder; + @Nullable private OpusDecoder decoder; + private int channelCount; + private int sampleRate; public LibopusAudioRenderer() { - this(null, null); + this(/* eventHandler= */ null, /* eventListener= */ null); } /** @@ -48,8 +51,8 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public LibopusAudioRenderer( - Handler eventHandler, - AudioRendererEventListener eventListener, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) { super(eventHandler, eventListener, audioProcessors); } @@ -67,8 +70,11 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { * has obtained the keys necessary to decrypt encrypted regions of the media. * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ - public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + public LibopusAudioRenderer( + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, AudioProcessor... audioProcessors) { super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys, audioProcessors); @@ -106,13 +112,25 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { initialInputBufferSize, format.initializationData, mediaCrypto); + channelCount = decoder.getChannelCount(); + sampleRate = decoder.getSampleRate(); return decoder; } @Override protected Format getOutputFormat() { - return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE, - Format.NO_VALUE, decoder.getChannelCount(), decoder.getSampleRate(), C.ENCODING_PCM_16BIT, - null, null, 0, null); + return Format.createAudioSampleFormat( + /* id= */ null, + MimeTypes.AUDIO_RAW, + /* codecs= */ null, + Format.NO_VALUE, + Format.NO_VALUE, + channelCount, + sampleRate, + C.ENCODING_PCM_16BIT, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); } } diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java index 5529701c06..d09d69bf03 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java @@ -68,7 +68,7 @@ public final class OpusLibrary { * protected content. */ public static boolean matchesExpectedExoMediaCryptoType( - Class exoMediaCryptoType) { + @Nullable Class exoMediaCryptoType) { return Util.areEqual(OpusLibrary.exoMediaCryptoType, exoMediaCryptoType); } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java index 5106ab67ad..e620332fc8 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java @@ -87,7 +87,7 @@ public final class VpxLibrary { * protected content. */ public static boolean matchesExpectedExoMediaCryptoType( - Class exoMediaCryptoType) { + @Nullable Class exoMediaCryptoType) { return Util.areEqual(VpxLibrary.exoMediaCryptoType, exoMediaCryptoType); } From d8c29e82114ad9a7768760c44e5d284bca8716ea Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 3 Jul 2019 09:22:45 +0100 Subject: [PATCH 1412/1556] Remove unnecessary warning suppression. PiperOrigin-RevId: 256320563 --- .../com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java index a97d99fa54..103c2176a0 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java @@ -47,8 +47,7 @@ import java.nio.ByteBuffer; @Nullable private ExtractorInput extractorInput; @Nullable private byte[] tempBuffer; private boolean endOfExtractorInput; - - @SuppressWarnings("nullness:method.invocation.invalid") + public FlacDecoderJni() throws FlacDecoderException { if (!FlacLibrary.isAvailable()) { throw new FlacDecoderException("Failed to load decoder native libraries."); From 0145edb60d218a939d869f0dbab0ab7fc0a34477 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 3 Jul 2019 15:07:55 +0100 Subject: [PATCH 1413/1556] Move MediaSource masking code into separate class. The masking logic for unprepared MediaSources is currently part of ConcatanatingMediaSource. Moving it to its own class nicely separates the code responsibilities and allows reuse. PiperOrigin-RevId: 256360904 --- .../source/ConcatenatingMediaSource.java | 274 ++------------- .../exoplayer2/source/MaskingMediaSource.java | 314 ++++++++++++++++++ .../source/ConcatenatingMediaSourceTest.java | 7 - 3 files changed, 344 insertions(+), 251 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java 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 bdf55fe40d..c72bed1b5b 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.Message; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; -import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder; @@ -71,8 +70,6 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceByUid; private final boolean isAtomic; private final boolean useLazyPreparation; - private final Timeline.Window window; - private final Timeline.Period period; private boolean timelineUpdateScheduled; private Set nextTimelineUpdateOnCompletionActions; @@ -136,8 +133,6 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource(); this.isAtomic = isAtomic; this.useLazyPreparation = useLazyPreparation; - window = new Timeline.Window(); - period = new Timeline.Period(); addMediaSources(Arrays.asList(mediaSources)); } @@ -435,33 +430,21 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceHolders = new ArrayList<>(mediaSources.size()); for (MediaSource mediaSource : mediaSources) { - mediaSourceHolders.add(new MediaSourceHolder(mediaSource)); + mediaSourceHolders.add(new MediaSourceHolder(mediaSource, useLazyPreparation)); } mediaSourcesPublic.addAll(index, mediaSourceHolders); if (playbackThreadHandler != null && !mediaSources.isEmpty()) { @@ -728,30 +711,23 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource 0) { MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1); + Timeline previousTimeline = previousHolder.mediaSource.getTimeline(); newMediaSourceHolder.reset( - newIndex, - previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount()); + newIndex, previousHolder.firstWindowIndexInChild + previousTimeline.getWindowCount()); } else { newMediaSourceHolder.reset(newIndex, /* firstWindowIndexInChild= */ 0); } - correctOffsets( - newIndex, /* childIndexUpdate= */ 1, newMediaSourceHolder.timeline.getWindowCount()); + Timeline newTimeline = newMediaSourceHolder.mediaSource.getTimeline(); + correctOffsets(newIndex, /* childIndexUpdate= */ 1, newTimeline.getWindowCount()); mediaSourceHolders.add(newIndex, newMediaSourceHolder); mediaSourceByUid.put(newMediaSourceHolder.uid, newMediaSourceHolder); - if (!useLazyPreparation) { - newMediaSourceHolder.hasStartedPreparing = true; - prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource); - } + prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource); } private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) { if (mediaSourceHolder == null) { throw new IllegalArgumentException(); } - DeferredTimeline deferredTimeline = mediaSourceHolder.timeline; - if (deferredTimeline.getTimeline() == timeline) { - return; - } if (mediaSourceHolder.childIndex + 1 < mediaSourceHolders.size()) { MediaSourceHolder nextHolder = mediaSourceHolders.get(mediaSourceHolder.childIndex + 1); int windowOffsetUpdate = @@ -762,61 +738,13 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource periodPosition = - timeline.getPeriodPosition(window, period, /* windowIndex= */ 0, windowStartPositionUs); - Object periodUid = periodPosition.first; - long periodPositionUs = periodPosition.second; - mediaSourceHolder.timeline = DeferredTimeline.createWithRealTimeline(timeline, periodUid); - if (deferredMediaPeriod != null) { - deferredMediaPeriod.overridePreparePositionUs(periodPositionUs); - MediaPeriodId idInSource = - deferredMediaPeriod.id.copyWithPeriodUid( - getChildPeriodUid(mediaSourceHolder, deferredMediaPeriod.id.periodUid)); - deferredMediaPeriod.createPeriod(idInSource); - } - } - mediaSourceHolder.isPrepared = true; scheduleTimelineUpdate(); } private void removeMediaSourceInternal(int index) { MediaSourceHolder holder = mediaSourceHolders.remove(index); mediaSourceByUid.remove(holder.uid); - Timeline oldTimeline = holder.timeline; + Timeline oldTimeline = holder.mediaSource.getTimeline(); correctOffsets(index, /* childIndexUpdate= */ -1, -oldTimeline.getWindowCount()); holder.isRemoved = true; maybeReleaseChildSource(holder); @@ -831,7 +759,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource activeMediaPeriods; + public final List activeMediaPeriodIds; - public DeferredTimeline timeline; public int childIndex; public int firstWindowIndexInChild; - public boolean hasStartedPreparing; - public boolean isPrepared; public boolean isRemoved; - public MediaSourceHolder(MediaSource mediaSource) { - this.mediaSource = mediaSource; - this.timeline = DeferredTimeline.createWithDummyTimeline(mediaSource.getTag()); - this.activeMediaPeriods = new ArrayList<>(); + public MediaSourceHolder(MediaSource mediaSource, boolean useLazyPreparation) { + this.mediaSource = new MaskingMediaSource(mediaSource, useLazyPreparation); + this.activeMediaPeriodIds = new ArrayList<>(); this.uid = new Object(); } public void reset(int childIndex, int firstWindowIndexInChild) { this.childIndex = childIndex; this.firstWindowIndexInChild = firstWindowIndexInChild; - this.hasStartedPreparing = false; - this.isPrepared = false; this.isRemoved = false; - this.activeMediaPeriods.clear(); + this.activeMediaPeriodIds.clear(); } } @@ -944,7 +859,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource { + + private final MediaSource mediaSource; + private final boolean useLazyPreparation; + private final Timeline.Window window; + private final Timeline.Period period; + + private MaskingTimeline timeline; + @Nullable private MaskingMediaPeriod unpreparedMaskingMediaPeriod; + private boolean hasStartedPreparing; + private boolean isPrepared; + + /** + * Creates the masking media source. + * + * @param mediaSource A {@link MediaSource}. + * @param useLazyPreparation Whether the {@code mediaSource} is prepared lazily. If false, all + * manifest loads and other initial preparation steps happen immediately. If true, these + * initial preparations are triggered only when the player starts buffering the media. + */ + public MaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation) { + this.mediaSource = mediaSource; + this.useLazyPreparation = useLazyPreparation; + window = new Timeline.Window(); + period = new Timeline.Period(); + timeline = MaskingTimeline.createWithDummyTimeline(mediaSource.getTag()); + } + + /** Returns the {@link Timeline}. */ + public Timeline getTimeline() { + return timeline; + } + + @Override + public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + if (!useLazyPreparation) { + hasStartedPreparing = true; + prepareChildSource(/* id= */ null, mediaSource); + } + } + + @Nullable + @Override + public Object getTag() { + return mediaSource.getTag(); + } + + @Override + @SuppressWarnings("MissingSuperCall") + public void maybeThrowSourceInfoRefreshError() throws IOException { + // Do nothing. Source info refresh errors will be thrown when calling + // MaskingMediaPeriod.maybeThrowPrepareError. + } + + @Override + public MaskingMediaPeriod createPeriod( + MediaPeriodId id, Allocator allocator, long startPositionUs) { + MaskingMediaPeriod mediaPeriod = + new MaskingMediaPeriod(mediaSource, id, allocator, startPositionUs); + if (isPrepared) { + MediaPeriodId idInSource = id.copyWithPeriodUid(getInternalPeriodUid(id.periodUid)); + mediaPeriod.createPeriod(idInSource); + } else { + // We should have at most one media period while source is unprepared because the duration is + // unset and we don't load beyond periods with unset duration. We need to figure out how to + // handle the prepare positions of multiple deferred media periods, should that ever change. + unpreparedMaskingMediaPeriod = mediaPeriod; + if (!hasStartedPreparing) { + hasStartedPreparing = true; + prepareChildSource(/* id= */ null, mediaSource); + } + } + return mediaPeriod; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + ((MaskingMediaPeriod) mediaPeriod).releasePeriod(); + unpreparedMaskingMediaPeriod = null; + } + + @Override + public void releaseSourceInternal() { + isPrepared = false; + hasStartedPreparing = false; + super.releaseSourceInternal(); + } + + @Override + protected void onChildSourceInfoRefreshed( + Void id, MediaSource mediaSource, Timeline newTimeline, @Nullable Object manifest) { + if (isPrepared) { + timeline = timeline.cloneWithUpdatedTimeline(newTimeline); + } else if (newTimeline.isEmpty()) { + timeline = + MaskingTimeline.createWithRealTimeline(newTimeline, MaskingTimeline.DUMMY_EXTERNAL_ID); + } else { + // Determine first period and the start position. + // This will be: + // 1. The default window start position if no deferred period has been created yet. + // 2. The non-zero prepare position of the deferred period under the assumption that this is + // a non-zero initial seek position in the window. + // 3. The default window start position if the deferred period has a prepare position of zero + // under the assumption that the prepare position of zero was used because it's the + // default position of the DummyTimeline window. Note that this will override an + // intentional seek to zero for a window with a non-zero default position. This is + // unlikely to be a problem as a non-zero default position usually only occurs for live + // playbacks and seeking to zero in a live window would cause BehindLiveWindowExceptions + // anyway. + newTimeline.getWindow(/* windowIndex= */ 0, window); + long windowStartPositionUs = window.getDefaultPositionUs(); + if (unpreparedMaskingMediaPeriod != null) { + long periodPreparePositionUs = unpreparedMaskingMediaPeriod.getPreparePositionUs(); + if (periodPreparePositionUs != 0) { + windowStartPositionUs = periodPreparePositionUs; + } + } + Pair periodPosition = + newTimeline.getPeriodPosition( + window, period, /* windowIndex= */ 0, windowStartPositionUs); + Object periodUid = periodPosition.first; + long periodPositionUs = periodPosition.second; + timeline = MaskingTimeline.createWithRealTimeline(newTimeline, periodUid); + if (unpreparedMaskingMediaPeriod != null) { + MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod; + unpreparedMaskingMediaPeriod = null; + maskingPeriod.overridePreparePositionUs(periodPositionUs); + MediaPeriodId idInSource = + maskingPeriod.id.copyWithPeriodUid(getInternalPeriodUid(maskingPeriod.id.periodUid)); + maskingPeriod.createPeriod(idInSource); + } + } + isPrepared = true; + refreshSourceInfo(this.timeline, manifest); + } + + @Nullable + @Override + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + Void id, MediaPeriodId mediaPeriodId) { + return mediaPeriodId.copyWithPeriodUid(getExternalPeriodUid(mediaPeriodId.periodUid)); + } + + private Object getInternalPeriodUid(Object externalPeriodUid) { + return externalPeriodUid.equals(MaskingTimeline.DUMMY_EXTERNAL_ID) + ? timeline.replacedInternalId + : externalPeriodUid; + } + + private Object getExternalPeriodUid(Object internalPeriodUid) { + return timeline.replacedInternalId.equals(internalPeriodUid) + ? MaskingTimeline.DUMMY_EXTERNAL_ID + : internalPeriodUid; + } + + /** + * Timeline used as placeholder for an unprepared media source. After preparation, a + * MaskingTimeline is used to keep the originally assigned dummy period ID. + */ + private static final class MaskingTimeline extends ForwardingTimeline { + + public static final Object DUMMY_EXTERNAL_ID = new Object(); + + private final Object replacedInternalId; + + /** + * Returns an instance with a dummy timeline using the provided window tag. + * + * @param windowTag A window tag. + */ + public static MaskingTimeline createWithDummyTimeline(@Nullable Object windowTag) { + return new MaskingTimeline(new DummyTimeline(windowTag), DUMMY_EXTERNAL_ID); + } + + /** + * Returns an instance with a real timeline, replacing the provided period ID with the already + * assigned dummy period ID. + * + * @param timeline The real timeline. + * @param firstPeriodUid The period UID in the timeline which will be replaced by the already + * assigned dummy period UID. + */ + public static MaskingTimeline createWithRealTimeline(Timeline timeline, Object firstPeriodUid) { + return new MaskingTimeline(timeline, firstPeriodUid); + } + + private MaskingTimeline(Timeline timeline, Object replacedInternalId) { + super(timeline); + this.replacedInternalId = replacedInternalId; + } + + /** + * Returns a copy with an updated timeline. This keeps the existing period replacement. + * + * @param timeline The new timeline. + */ + public MaskingTimeline cloneWithUpdatedTimeline(Timeline timeline) { + return new MaskingTimeline(timeline, replacedInternalId); + } + + /** Returns the wrapped timeline. */ + public Timeline getTimeline() { + return timeline; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + timeline.getPeriod(periodIndex, period, setIds); + if (Util.areEqual(period.uid, replacedInternalId)) { + period.uid = DUMMY_EXTERNAL_ID; + } + return period; + } + + @Override + public int getIndexOfPeriod(Object uid) { + return timeline.getIndexOfPeriod(DUMMY_EXTERNAL_ID.equals(uid) ? replacedInternalId : uid); + } + + @Override + public Object getUidOfPeriod(int periodIndex) { + Object uid = timeline.getUidOfPeriod(periodIndex); + return Util.areEqual(uid, replacedInternalId) ? DUMMY_EXTERNAL_ID : uid; + } + } + + /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ + private static final class DummyTimeline extends Timeline { + + @Nullable private final Object tag; + + public DummyTimeline(@Nullable Object tag) { + this.tag = tag; + } + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow( + int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + return window.set( + tag, + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + /* isSeekable= */ false, + // Dynamic window to indicate pending timeline updates. + /* isDynamic= */ true, + /* defaultPositionUs= */ 0, + /* durationUs= */ C.TIME_UNSET, + /* firstPeriodIndex= */ 0, + /* lastPeriodIndex= */ 0, + /* positionInFirstPeriodUs= */ 0); + } + + @Override + public int getPeriodCount() { + return 1; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + return period.set( + /* id= */ 0, + /* uid= */ MaskingTimeline.DUMMY_EXTERNAL_ID, + /* windowIndex= */ 0, + /* durationUs = */ C.TIME_UNSET, + /* positionInWindowUs= */ 0); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return uid == MaskingTimeline.DUMMY_EXTERNAL_ID ? 0 : C.INDEX_UNSET; + } + + @Override + public Object getUidOfPeriod(int periodIndex) { + return MaskingTimeline.DUMMY_EXTERNAL_ID; + } + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 8137289555..5187addec3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -279,13 +279,6 @@ public final class ConcatenatingMediaSourceTest { CountDownLatch preparedCondition = testRunner.preparePeriod(lazyPeriod, 0); assertThat(preparedCondition.getCount()).isEqualTo(1); - // Assert that a second period can also be created and released without problems. - MediaPeriod secondLazyPeriod = - testRunner.createPeriod( - new MediaPeriodId( - timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); - testRunner.releasePeriod(secondLazyPeriod); - // Trigger source info refresh for lazy media source. Assert that now all information is // available again and the previously created period now also finished preparing. testRunner.runOnPlaybackThread( From 1060c767fcb491780095a02f670f8e76850a885c Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jul 2019 11:23:52 +0100 Subject: [PATCH 1414/1556] Simplify FlacExtractor (step toward enabling nullness checking) - Inline some unnecessarily split out helper methods - Clear ExtractorInput from FlacDecoderJni data after usage - Clean up exception handling for StreamInfo decode failures PiperOrigin-RevId: 256524955 --- .../ext/flac/FlacBinarySearchSeekerTest.java | 4 +- .../ext/flac/FlacExtractorTest.java | 2 +- .../exoplayer2/ext/flac/FlacDecoder.java | 8 +- .../exoplayer2/ext/flac/FlacDecoderJni.java | 37 ++-- .../exoplayer2/ext/flac/FlacExtractor.java | 192 ++++++++---------- 5 files changed, 120 insertions(+), 123 deletions(-) diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java index 435279fc45..934d7cf106 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java @@ -52,7 +52,7 @@ public final class FlacBinarySearchSeekerTest { FlacBinarySearchSeeker seeker = new FlacBinarySearchSeeker( - decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); + decoderJni.decodeStreamInfo(), /* firstFramePosition= */ 0, data.length, decoderJni); SeekMap seekMap = seeker.getSeekMap(); assertThat(seekMap).isNotNull(); @@ -70,7 +70,7 @@ public final class FlacBinarySearchSeekerTest { decoderJni.setData(input); FlacBinarySearchSeeker seeker = new FlacBinarySearchSeeker( - decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); + decoderJni.decodeStreamInfo(), /* firstFramePosition= */ 0, data.length, decoderJni); seeker.setSeekTargetUs(/* timeUs= */ 1000); assertThat(seeker.isSeeking()).isTrue(); diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java index d9cbac6ad5..97f152cea4 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java @@ -28,7 +28,7 @@ import org.junit.runner.RunWith; public class FlacExtractorTest { @Before - public void setUp() throws Exception { + public void setUp() { if (!FlacLibrary.isAvailable()) { fail("Flac library not available."); } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java index 9b15aff846..d20c18e957 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.flac; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; @@ -59,14 +60,13 @@ import java.util.List; decoderJni.setData(ByteBuffer.wrap(initializationData.get(0))); FlacStreamInfo streamInfo; try { - streamInfo = decoderJni.decodeMetadata(); + streamInfo = decoderJni.decodeStreamInfo(); + } catch (ParserException e) { + throw new FlacDecoderException("Failed to decode StreamInfo", e); } catch (IOException | InterruptedException e) { // Never happens. throw new IllegalStateException(e); } - if (streamInfo == null) { - throw new FlacDecoderException("Metadata decoding failed"); - } int initialInputBufferSize = maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize; diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java index 103c2176a0..32ef22dab0 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.flac; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.Util; @@ -47,7 +48,7 @@ import java.nio.ByteBuffer; @Nullable private ExtractorInput extractorInput; @Nullable private byte[] tempBuffer; private boolean endOfExtractorInput; - + public FlacDecoderJni() throws FlacDecoderException { if (!FlacLibrary.isAvailable()) { throw new FlacDecoderException("Failed to load decoder native libraries."); @@ -59,37 +60,46 @@ import java.nio.ByteBuffer; } /** - * Sets data to be parsed by libflac. + * Sets the data to be parsed. * * @param byteBufferData Source {@link ByteBuffer}. */ public void setData(ByteBuffer byteBufferData) { this.byteBufferData = byteBufferData; this.extractorInput = null; - this.tempBuffer = null; } /** - * Sets data to be parsed by libflac. + * Sets the data to be parsed. * * @param extractorInput Source {@link ExtractorInput}. */ public void setData(ExtractorInput extractorInput) { this.byteBufferData = null; this.extractorInput = extractorInput; - if (tempBuffer == null) { - this.tempBuffer = new byte[TEMP_BUFFER_SIZE]; - } endOfExtractorInput = false; + if (tempBuffer == null) { + tempBuffer = new byte[TEMP_BUFFER_SIZE]; + } } + /** + * Returns whether the end of the data to be parsed has been reached, or true if no data was set. + */ public boolean isEndOfData() { if (byteBufferData != null) { return byteBufferData.remaining() == 0; } else if (extractorInput != null) { return endOfExtractorInput; + } else { + return true; } - return true; + } + + /** Clears the data to be parsed. */ + public void clearData() { + byteBufferData = null; + extractorInput = null; } /** @@ -98,12 +108,11 @@ import java.nio.ByteBuffer; *

        This method blocks until at least one byte of data can be read, the end of the input is * detected or an exception is thrown. * - *

        This method is called from the native code. - * * @param target A target {@link ByteBuffer} into which data should be written. * @return Returns the number of bytes read, or -1 on failure. If all of the data has already been * read from the source, then 0 is returned. */ + @SuppressWarnings("unused") // Called from native code. public int read(ByteBuffer target) throws IOException, InterruptedException { int byteCount = target.remaining(); if (byteBufferData != null) { @@ -134,8 +143,12 @@ import java.nio.ByteBuffer; } /** Decodes and consumes the StreamInfo section from the FLAC stream. */ - public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException { - return flacDecodeMetadata(nativeDecoderContext); + public FlacStreamInfo decodeStreamInfo() throws IOException, InterruptedException { + FlacStreamInfo streamInfo = flacDecodeMetadata(nativeDecoderContext); + if (streamInfo == null) { + throw new ParserException("Failed to decode StreamInfo"); + } + return streamInfo; } /** diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 79350e6ae3..491b962129 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -21,7 +21,7 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.extractor.BinarySearchSeeker; +import com.google.android.exoplayer2.extractor.BinarySearchSeeker.OutputFrameHolder; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -75,22 +75,19 @@ public final class FlacExtractor implements Extractor { private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22}; private final Id3Peeker id3Peeker; - private final boolean isId3MetadataDisabled; + private final boolean id3MetadataDisabled; - private FlacDecoderJni decoderJni; + @Nullable private FlacDecoderJni decoderJni; + @Nullable private ExtractorOutput extractorOutput; + @Nullable private TrackOutput trackOutput; - private ExtractorOutput extractorOutput; - private TrackOutput trackOutput; + private boolean streamInfoDecoded; + @Nullable private FlacStreamInfo streamInfo; + @Nullable private ParsableByteArray outputBuffer; + @Nullable private OutputFrameHolder outputFrameHolder; - private ParsableByteArray outputBuffer; - private ByteBuffer outputByteBuffer; - private BinarySearchSeeker.OutputFrameHolder outputFrameHolder; - private FlacStreamInfo streamInfo; - - private Metadata id3Metadata; - @Nullable private FlacBinarySearchSeeker flacBinarySearchSeeker; - - private boolean readPastStreamInfo; + @Nullable private Metadata id3Metadata; + @Nullable private FlacBinarySearchSeeker binarySearchSeeker; /** Constructs an instance with flags = 0. */ public FlacExtractor() { @@ -104,7 +101,7 @@ public final class FlacExtractor implements Extractor { */ public FlacExtractor(int flags) { id3Peeker = new Id3Peeker(); - isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; + id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; } @Override @@ -130,48 +127,53 @@ public final class FlacExtractor implements Extractor { @Override public int read(final ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { - if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) { + if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) { id3Metadata = peekId3Data(input); } decoderJni.setData(input); - readPastStreamInfo(input); - - if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) { - return handlePendingSeek(input, seekPosition); - } - - long lastDecodePosition = decoderJni.getDecodePosition(); try { - decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition); - } catch (FlacDecoderJni.FlacFrameDecodeException e) { - throw new IOException("Cannot read frame at position " + lastDecodePosition, e); - } - int outputSize = outputByteBuffer.limit(); - if (outputSize == 0) { - return RESULT_END_OF_INPUT; - } + decodeStreamInfo(input); - writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp()); - return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; + if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) { + return handlePendingSeek(input, seekPosition); + } + + ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; + long lastDecodePosition = decoderJni.getDecodePosition(); + try { + decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition); + } catch (FlacDecoderJni.FlacFrameDecodeException e) { + throw new IOException("Cannot read frame at position " + lastDecodePosition, e); + } + int outputSize = outputByteBuffer.limit(); + if (outputSize == 0) { + return RESULT_END_OF_INPUT; + } + + outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp()); + return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; + } finally { + decoderJni.clearData(); + } } @Override public void seek(long position, long timeUs) { if (position == 0) { - readPastStreamInfo = false; + streamInfoDecoded = false; } if (decoderJni != null) { decoderJni.reset(position); } - if (flacBinarySearchSeeker != null) { - flacBinarySearchSeeker.setSeekTargetUs(timeUs); + if (binarySearchSeeker != null) { + binarySearchSeeker.setSeekTargetUs(timeUs); } } @Override public void release() { - flacBinarySearchSeeker = null; + binarySearchSeeker = null; if (decoderJni != null) { decoderJni.release(); decoderJni = null; @@ -179,16 +181,15 @@ public final class FlacExtractor implements Extractor { } /** - * Peeks ID3 tag data (if present) at the beginning of the input. + * Peeks ID3 tag data at the beginning of the input. * - * @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not - * present in the input. + * @return The first ID3 tag {@link Metadata}, or null if an ID3 tag is not present in the input. */ @Nullable private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException { input.resetPeekPosition(); Id3Decoder.FramePredicate id3FramePredicate = - isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null; + id3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null; return id3Peeker.peekId3Data(input, id3FramePredicate); } @@ -199,68 +200,61 @@ public final class FlacExtractor implements Extractor { */ private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException { byte[] header = new byte[FLAC_SIGNATURE.length]; - input.peekFully(header, 0, FLAC_SIGNATURE.length); + input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length); return Arrays.equals(header, FLAC_SIGNATURE); } - private void readPastStreamInfo(ExtractorInput input) throws InterruptedException, IOException { - if (readPastStreamInfo) { + private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, IOException { + if (streamInfoDecoded) { return; } - FlacStreamInfo streamInfo = decodeStreamInfo(input); - readPastStreamInfo = true; - if (this.streamInfo == null) { - updateFlacStreamInfo(input, streamInfo); - } - } - - private void updateFlacStreamInfo(ExtractorInput input, FlacStreamInfo streamInfo) { - this.streamInfo = streamInfo; - outputSeekMap(input, streamInfo); - outputFormat(streamInfo); - outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); - outputByteBuffer = ByteBuffer.wrap(outputBuffer.data); - outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer); - } - - private FlacStreamInfo decodeStreamInfo(ExtractorInput input) - throws InterruptedException, IOException { + FlacStreamInfo streamInfo; try { - FlacStreamInfo streamInfo = decoderJni.decodeMetadata(); - if (streamInfo == null) { - throw new IOException("Metadata decoding failed"); - } - return streamInfo; + streamInfo = decoderJni.decodeStreamInfo(); } catch (IOException e) { - decoderJni.reset(0); - input.setRetryPosition(0, e); + decoderJni.reset(/* newPosition= */ 0); + input.setRetryPosition(/* position= */ 0, e); throw e; } + + streamInfoDecoded = true; + if (this.streamInfo == null) { + this.streamInfo = streamInfo; + outputSeekMap(streamInfo, input.getLength()); + outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata); + outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); + outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); + } } - private void outputSeekMap(ExtractorInput input, FlacStreamInfo streamInfo) { - boolean hasSeekTable = decoderJni.getSeekPosition(0) != -1; - SeekMap seekMap = - hasSeekTable - ? new FlacSeekMap(streamInfo.durationUs(), decoderJni) - : getSeekMapForNonSeekTableFlac(input, streamInfo); + private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition) + throws InterruptedException, IOException { + int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder); + ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; + if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { + outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs); + } + return seekResult; + } + + private void outputSeekMap(FlacStreamInfo streamInfo, long inputLength) { + boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1; + SeekMap seekMap; + if (hasSeekTable) { + seekMap = new FlacSeekMap(streamInfo.durationUs(), decoderJni); + } else if (inputLength != C.LENGTH_UNSET) { + long firstFramePosition = decoderJni.getDecodePosition(); + binarySearchSeeker = + new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni); + seekMap = binarySearchSeeker.getSeekMap(); + } else { + seekMap = new SeekMap.Unseekable(streamInfo.durationUs()); + } extractorOutput.seekMap(seekMap); } - private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) { - long inputLength = input.getLength(); - if (inputLength != C.LENGTH_UNSET) { - long firstFramePosition = decoderJni.getDecodePosition(); - flacBinarySearchSeeker = - new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni); - return flacBinarySearchSeeker.getSeekMap(); - } else { // can't seek at all, because there's no SeekTable and the input length is unknown. - return new SeekMap.Unseekable(streamInfo.durationUs()); - } - } - - private void outputFormat(FlacStreamInfo streamInfo) { + private void outputFormat(FlacStreamInfo streamInfo, Metadata metadata) { Format mediaFormat = Format.createAudioSampleFormat( /* id= */ null, @@ -277,25 +271,15 @@ public final class FlacExtractor implements Extractor { /* drmInitData= */ null, /* selectionFlags= */ 0, /* language= */ null, - isId3MetadataDisabled ? null : id3Metadata); + metadata); trackOutput.format(mediaFormat); } - private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition) - throws InterruptedException, IOException { - int seekResult = - flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder); - ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; - if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { - writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs); - } - return seekResult; - } - - private void writeLastSampleToOutput(int size, long lastSampleTimestamp) { - outputBuffer.setPosition(0); - trackOutput.sampleData(outputBuffer, size); - trackOutput.sampleMetadata(lastSampleTimestamp, C.BUFFER_FLAG_KEY_FRAME, size, 0, null); + private void outputSample(ParsableByteArray sampleData, int size, long timeUs) { + sampleData.setPosition(0); + trackOutput.sampleData(sampleData, size); + trackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null); } /** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */ From 948d69b774ec23136431c4142bd82947a35a6981 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jul 2019 11:32:20 +0100 Subject: [PATCH 1415/1556] Make FlacExtractor output methods static This gives a caller greater confidence that the methods have no side effects, and remove any nullness issues with these methods accessing @Nullable member variables. PiperOrigin-RevId: 256525739 --- .../exoplayer2/ext/flac/FlacExtractor.java | 65 ++++++++++++------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 491b962129..b50554e2f6 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -151,7 +152,7 @@ public final class FlacExtractor implements Extractor { return RESULT_END_OF_INPUT; } - outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp()); + outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp(), trackOutput); return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; } finally { decoderJni.clearData(); @@ -193,17 +194,6 @@ public final class FlacExtractor implements Extractor { return id3Peeker.peekId3Data(input, id3FramePredicate); } - /** - * Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present. - * - * @return Whether the input begins with {@link #FLAC_SIGNATURE}. - */ - private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException { - byte[] header = new byte[FLAC_SIGNATURE.length]; - input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length); - return Arrays.equals(header, FLAC_SIGNATURE); - } - private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, IOException { if (streamInfoDecoded) { return; @@ -221,8 +211,9 @@ public final class FlacExtractor implements Extractor { streamInfoDecoded = true; if (this.streamInfo == null) { this.streamInfo = streamInfo; - outputSeekMap(streamInfo, input.getLength()); - outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata); + binarySearchSeeker = + outputSeekMap(decoderJni, streamInfo, input.getLength(), extractorOutput); + outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata, trackOutput); outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); } @@ -230,31 +221,56 @@ public final class FlacExtractor implements Extractor { private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition) throws InterruptedException, IOException { + Assertions.checkNotNull(binarySearchSeeker); int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder); ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { - outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs); + outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs, trackOutput); } return seekResult; } - private void outputSeekMap(FlacStreamInfo streamInfo, long inputLength) { + /** + * Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present. + * + * @return Whether the input begins with {@link #FLAC_SIGNATURE}. + */ + private static boolean peekFlacSignature(ExtractorInput input) + throws IOException, InterruptedException { + byte[] header = new byte[FLAC_SIGNATURE.length]; + input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length); + return Arrays.equals(header, FLAC_SIGNATURE); + } + + /** + * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to + * handle seeks. + */ + @Nullable + private static FlacBinarySearchSeeker outputSeekMap( + FlacDecoderJni decoderJni, + FlacStreamInfo streamInfo, + long streamLength, + ExtractorOutput output) { boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1; + FlacBinarySearchSeeker binarySearchSeeker = null; SeekMap seekMap; if (hasSeekTable) { seekMap = new FlacSeekMap(streamInfo.durationUs(), decoderJni); - } else if (inputLength != C.LENGTH_UNSET) { + } else if (streamLength != C.LENGTH_UNSET) { long firstFramePosition = decoderJni.getDecodePosition(); binarySearchSeeker = - new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni); + new FlacBinarySearchSeeker(streamInfo, firstFramePosition, streamLength, decoderJni); seekMap = binarySearchSeeker.getSeekMap(); } else { seekMap = new SeekMap.Unseekable(streamInfo.durationUs()); } - extractorOutput.seekMap(seekMap); + output.seekMap(seekMap); + return binarySearchSeeker; } - private void outputFormat(FlacStreamInfo streamInfo, Metadata metadata) { + private static void outputFormat( + FlacStreamInfo streamInfo, Metadata metadata, TrackOutput output) { Format mediaFormat = Format.createAudioSampleFormat( /* id= */ null, @@ -272,13 +288,14 @@ public final class FlacExtractor implements Extractor { /* selectionFlags= */ 0, /* language= */ null, metadata); - trackOutput.format(mediaFormat); + output.format(mediaFormat); } - private void outputSample(ParsableByteArray sampleData, int size, long timeUs) { + private static void outputSample( + ParsableByteArray sampleData, int size, long timeUs, TrackOutput output) { sampleData.setPosition(0); - trackOutput.sampleData(sampleData, size); - trackOutput.sampleMetadata( + output.sampleData(sampleData, size); + output.sampleMetadata( timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null); } From 0f364dfffc0c9af03316f473edd2ee49b7f20a2b Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jul 2019 11:38:48 +0100 Subject: [PATCH 1416/1556] Remove FlacExtractor from nullness blacklist PiperOrigin-RevId: 256526365 --- extensions/flac/build.gradle | 1 + .../exoplayer2/ext/flac/FlacExtractor.java | 42 ++++++++++++++----- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 06a5888404..10b244cb39 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -40,6 +40,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.0.2' + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion testImplementation project(modulePrefix + 'testutils-robolectric') diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index b50554e2f6..082068f34d 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -43,6 +43,9 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.util.Arrays; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Facilitates the extraction of data from the FLAC container format. @@ -75,17 +78,17 @@ public final class FlacExtractor implements Extractor { */ private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22}; + private final ParsableByteArray outputBuffer; private final Id3Peeker id3Peeker; private final boolean id3MetadataDisabled; @Nullable private FlacDecoderJni decoderJni; - @Nullable private ExtractorOutput extractorOutput; - @Nullable private TrackOutput trackOutput; + private @MonotonicNonNull ExtractorOutput extractorOutput; + private @MonotonicNonNull TrackOutput trackOutput; private boolean streamInfoDecoded; - @Nullable private FlacStreamInfo streamInfo; - @Nullable private ParsableByteArray outputBuffer; - @Nullable private OutputFrameHolder outputFrameHolder; + private @MonotonicNonNull FlacStreamInfo streamInfo; + private @MonotonicNonNull OutputFrameHolder outputFrameHolder; @Nullable private Metadata id3Metadata; @Nullable private FlacBinarySearchSeeker binarySearchSeeker; @@ -101,6 +104,7 @@ public final class FlacExtractor implements Extractor { * @param flags Flags that control the extractor's behavior. */ public FlacExtractor(int flags) { + outputBuffer = new ParsableByteArray(); id3Peeker = new Id3Peeker(); id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; } @@ -132,12 +136,12 @@ public final class FlacExtractor implements Extractor { id3Metadata = peekId3Data(input); } - decoderJni.setData(input); + FlacDecoderJni decoderJni = initDecoderJni(input); try { decodeStreamInfo(input); if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) { - return handlePendingSeek(input, seekPosition); + return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput); } ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; @@ -194,6 +198,17 @@ public final class FlacExtractor implements Extractor { return id3Peeker.peekId3Data(input, id3FramePredicate); } + @EnsuresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Ensures initialized. + @SuppressWarnings({"contracts.postcondition.not.satisfied"}) + private FlacDecoderJni initDecoderJni(ExtractorInput input) { + FlacDecoderJni decoderJni = Assertions.checkNotNull(this.decoderJni); + decoderJni.setData(input); + return decoderJni; + } + + @RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized. + @EnsuresNonNull({"streamInfo", "outputFrameHolder"}) // Ensures StreamInfo decoded. + @SuppressWarnings({"contracts.postcondition.not.satisfied"}) private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, IOException { if (streamInfoDecoded) { return; @@ -214,14 +229,19 @@ public final class FlacExtractor implements Extractor { binarySearchSeeker = outputSeekMap(decoderJni, streamInfo, input.getLength(), extractorOutput); outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata, trackOutput); - outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); + outputBuffer.reset(streamInfo.maxDecodedFrameSize()); outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); } } - private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition) + @RequiresNonNull("binarySearchSeeker") + private int handlePendingSeek( + ExtractorInput input, + PositionHolder seekPosition, + ParsableByteArray outputBuffer, + OutputFrameHolder outputFrameHolder, + TrackOutput trackOutput) throws InterruptedException, IOException { - Assertions.checkNotNull(binarySearchSeeker); int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder); ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { @@ -270,7 +290,7 @@ public final class FlacExtractor implements Extractor { } private static void outputFormat( - FlacStreamInfo streamInfo, Metadata metadata, TrackOutput output) { + FlacStreamInfo streamInfo, @Nullable Metadata metadata, TrackOutput output) { Format mediaFormat = Format.createAudioSampleFormat( /* id= */ null, From 81e7b31be187c1ca7de2129d942b62fdff0079d6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 4 Jul 2019 12:54:56 +0100 Subject: [PATCH 1417/1556] Apply playback parameters in a consistent way. Currently, we sometimes apply new playback parameters directly and sometimes through the list of playbackParameterCheckpoints. Only when using the checkpoints, we also reset the offset and corresponding position for speedup position calculation. However, these offsets need to be changed in all cases to prevent calculation errors during speedup calculation[1]. This change channels all playback parameters changes through the checkpoints to ensure the offsets get updated accordingly. This fixes an issue introduced in https://github.com/google/ExoPlayer/commit/31911ca54a13b0003d6cf902b95c2ed445afa930. [1] - The speed up is calculated using the ratio of input and output bytes in SonicAudioProcessor.scaleDurationForSpeedUp. Whenever we set new playback parameters to the audio processor these two counts are reset. If we don't reset the offsets too, the scaled timestamp can be a large value compared to the input and output bytes causing massive inaccuracies (like the +20 seconds in the linked issue). Issue:#6117 PiperOrigin-RevId: 256533780 --- RELEASENOTES.md | 7 ++- .../exoplayer2/audio/DefaultAudioSink.java | 47 ++++++++++--------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d76ca54b7b..855b0e94f2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -12,8 +12,11 @@ checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Decoders: Prefer decoders that advertise format support over ones that do not, even if they are listed lower in the `MediaCodecList`. -* Audio: Fix an issue where not all audio was played out when the configuration - for the underlying track was changing (e.g., at some period transitions). +* Audio: + * Fix an issue where not all audio was played out when the configuration + for the underlying track was changing (e.g., at some period transitions). + * Fix an issue where playback speed was applied inaccurately in playlists + ([#6117](https://github.com/google/ExoPlayer/issues/6117)). * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). * Add VR player demo. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index f982efa9a7..e3f753958e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -501,7 +501,7 @@ public final class DefaultAudioSink implements AudioSink { } } - private void initialize() throws InitializationException { + private void initialize(long presentationTimeUs) throws InitializationException { // If we're asynchronously releasing a previous audio track then we block until it has been // released. This guarantees that we cannot end up in a state where we have multiple audio // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust @@ -533,11 +533,7 @@ public final class DefaultAudioSink implements AudioSink { } } - playbackParameters = - configuration.canApplyPlaybackParameters - ? audioProcessorChain.applyPlaybackParameters(playbackParameters) - : PlaybackParameters.DEFAULT; - setupAudioProcessors(); + applyPlaybackParameters(playbackParameters, presentationTimeUs); audioTrackPositionTracker.setAudioTrack( audioTrack, @@ -591,15 +587,12 @@ public final class DefaultAudioSink implements AudioSink { configuration = pendingConfiguration; pendingConfiguration = null; } - playbackParameters = - configuration.canApplyPlaybackParameters - ? audioProcessorChain.applyPlaybackParameters(playbackParameters) - : PlaybackParameters.DEFAULT; - setupAudioProcessors(); + // Re-apply playback parameters. + applyPlaybackParameters(playbackParameters, presentationTimeUs); } if (!isInitialized()) { - initialize(); + initialize(presentationTimeUs); if (playing) { play(); } @@ -635,15 +628,7 @@ public final class DefaultAudioSink implements AudioSink { } PlaybackParameters newPlaybackParameters = afterDrainPlaybackParameters; afterDrainPlaybackParameters = null; - newPlaybackParameters = audioProcessorChain.applyPlaybackParameters(newPlaybackParameters); - // Store the position and corresponding media time from which the parameters will apply. - playbackParametersCheckpoints.add( - new PlaybackParametersCheckpoint( - newPlaybackParameters, - Math.max(0, presentationTimeUs), - configuration.framesToDurationUs(getWrittenFrames()))); - // Update the set of active audio processors to take into account the new parameters. - setupAudioProcessors(); + applyPlaybackParameters(newPlaybackParameters, presentationTimeUs); } if (startMediaTimeState == START_NOT_SET) { @@ -857,8 +842,9 @@ public final class DefaultAudioSink implements AudioSink { // parameters apply. afterDrainPlaybackParameters = playbackParameters; } else { - // Update the playback parameters now. - this.playbackParameters = audioProcessorChain.applyPlaybackParameters(playbackParameters); + // Update the playback parameters now. They will be applied to the audio processors during + // initialization. + this.playbackParameters = playbackParameters; } } return this.playbackParameters; @@ -1040,6 +1026,21 @@ public final class DefaultAudioSink implements AudioSink { }.start(); } + private void applyPlaybackParameters( + PlaybackParameters playbackParameters, long presentationTimeUs) { + PlaybackParameters newPlaybackParameters = + configuration.canApplyPlaybackParameters + ? audioProcessorChain.applyPlaybackParameters(playbackParameters) + : PlaybackParameters.DEFAULT; + // Store the position and corresponding media time from which the parameters will apply. + playbackParametersCheckpoints.add( + new PlaybackParametersCheckpoint( + newPlaybackParameters, + /* mediaTimeUs= */ Math.max(0, presentationTimeUs), + /* positionUs= */ configuration.framesToDurationUs(getWrittenFrames()))); + setupAudioProcessors(); + } + private long applySpeedup(long positionUs) { @Nullable PlaybackParametersCheckpoint checkpoint = null; while (!playbackParametersCheckpoints.isEmpty() From b1790f9dccffd09fa6de91b8b11ee167af314f37 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 4 Jul 2019 14:55:39 +0100 Subject: [PATCH 1418/1556] Fix IMA test build issue PiperOrigin-RevId: 256545951 --- .../google/android/exoplayer2/ext/ima/FakeAdsRequest.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java index 3c34d9b577..7c2c8a6e0b 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java @@ -105,11 +105,6 @@ public final class FakeAdsRequest implements AdsRequest { throw new UnsupportedOperationException(); } - @Override - public void setContinuousPlayback(boolean b) { - throw new UnsupportedOperationException(); - } - @Override public void setContentDuration(float v) { throw new UnsupportedOperationException(); From 924cfac96657c70c1c97cfd906952af62d6a8160 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jul 2019 20:02:20 +0100 Subject: [PATCH 1419/1556] Remove more low hanging fruit from nullness blacklist PiperOrigin-RevId: 256573352 --- .../extractor/flv/ScriptTagPayloadReader.java | 22 ++++++++++++++----- .../exoplayer2/extractor/mp4/Track.java | 1 + .../extractor/mp4/TrackEncryptionBox.java | 2 +- .../google/android/exoplayer2/text/Cue.java | 7 +++--- .../text/SimpleSubtitleDecoder.java | 2 ++ .../exoplayer2/text/SubtitleOutputBuffer.java | 9 ++++---- .../exoplayer2/text/pgs/PgsDecoder.java | 5 ++++- .../exoplayer2/text/ssa/SsaDecoder.java | 7 +++--- .../exoplayer2/text/ssa/SsaSubtitle.java | 4 ++-- .../exoplayer2/text/subrip/SubripDecoder.java | 6 +++-- .../text/subrip/SubripSubtitle.java | 4 ++-- .../exoplayer2/text/tx3g/Tx3gDecoder.java | 16 +++++++++----- 12 files changed, 57 insertions(+), 28 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java index eb1cc8f336..806cc9fad4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2.extractor.flv; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; import java.util.Date; @@ -44,7 +46,7 @@ import java.util.Map; private long durationUs; public ScriptTagPayloadReader() { - super(null); + super(new DummyTrackOutput()); durationUs = C.TIME_UNSET; } @@ -138,7 +140,10 @@ import java.util.Map; ArrayList list = new ArrayList<>(count); for (int i = 0; i < count; i++) { int type = readAmfType(data); - list.add(readAmfData(data, type)); + Object value = readAmfData(data, type); + if (value != null) { + list.add(value); + } } return list; } @@ -157,7 +162,10 @@ import java.util.Map; if (type == AMF_TYPE_END_MARKER) { break; } - array.put(key, readAmfData(data, type)); + Object value = readAmfData(data, type); + if (value != null) { + array.put(key, value); + } } return array; } @@ -174,7 +182,10 @@ import java.util.Map; for (int i = 0; i < count; i++) { String key = readAmfString(data); int type = readAmfType(data); - array.put(key, readAmfData(data, type)); + Object value = readAmfData(data, type); + if (value != null) { + array.put(key, value); + } } return array; } @@ -191,6 +202,7 @@ import java.util.Map; return date; } + @Nullable private static Object readAmfData(ParsableByteArray data, int type) { switch (type) { case AMF_TYPE_NUMBER: @@ -208,8 +220,8 @@ import java.util.Map; case AMF_TYPE_DATE: return readAmfDate(data); default: + // We don't log a warning because there are types that we knowingly don't support. return null; } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java index 9d3635e8b3..7676926c4d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java @@ -123,6 +123,7 @@ public final class Track { * @return The {@link TrackEncryptionBox} for the given sample description index. Maybe null if no * such entry exists. */ + @Nullable public TrackEncryptionBox getSampleDescriptionEncryptionBox(int sampleDescriptionIndex) { return sampleDescriptionEncryptionBoxes == null ? null : sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java index 5bd29c6e75..a35d211aa4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java @@ -52,7 +52,7 @@ public final class TrackEncryptionBox { * If {@link #perSampleIvSize} is 0, holds the default initialization vector as defined in the * track encryption box or sample group description box. Null otherwise. */ - public final byte[] defaultInitializationVector; + @Nullable public final byte[] defaultInitializationVector; /** * @param isEncrypted See {@link #isEncrypted}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java index 29facdb210..39359a9367 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -29,9 +29,10 @@ import java.lang.annotation.RetentionPolicy; */ public class Cue { - /** - * An unset position or width. - */ + /** The empty cue. */ + public static final Cue EMPTY = new Cue(""); + + /** An unset position or width. */ public static final float DIMEN_UNSET = Float.MIN_VALUE; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java index 38d6ff25cb..bd561afaf8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.SimpleDecoder; import java.nio.ByteBuffer; @@ -69,6 +70,7 @@ public abstract class SimpleSubtitleDecoder extends @SuppressWarnings("ByteBufferBackingArray") @Override + @Nullable protected final SubtitleDecoderException decode( SubtitleInputBuffer inputBuffer, SubtitleOutputBuffer outputBuffer, boolean reset) { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java index b34628b922..1dcdecf95f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.text; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.decoder.OutputBuffer; +import com.google.android.exoplayer2.util.Assertions; import java.util.List; /** @@ -46,22 +47,22 @@ public abstract class SubtitleOutputBuffer extends OutputBuffer implements Subti @Override public int getEventTimeCount() { - return subtitle.getEventTimeCount(); + return Assertions.checkNotNull(subtitle).getEventTimeCount(); } @Override public long getEventTime(int index) { - return subtitle.getEventTime(index) + subsampleOffsetUs; + return Assertions.checkNotNull(subtitle).getEventTime(index) + subsampleOffsetUs; } @Override public int getNextEventTimeIndex(long timeUs) { - return subtitle.getNextEventTimeIndex(timeUs - subsampleOffsetUs); + return Assertions.checkNotNull(subtitle).getNextEventTimeIndex(timeUs - subsampleOffsetUs); } @Override public List getCues(long timeUs) { - return subtitle.getCues(timeUs - subsampleOffsetUs); + return Assertions.checkNotNull(subtitle).getCues(timeUs - subsampleOffsetUs); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java index 091bda49f3..9ef3556c8f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.text.pgs; import android.graphics.Bitmap; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.Subtitle; @@ -41,7 +42,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { private final ParsableByteArray inflatedBuffer; private final CueBuilder cueBuilder; - private Inflater inflater; + @Nullable private Inflater inflater; public PgsDecoder() { super("PgsDecoder"); @@ -76,6 +77,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { } } + @Nullable private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) { int limit = buffer.limit(); int sectionType = buffer.readUnsignedByte(); @@ -197,6 +199,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { bitmapY = buffer.readUnsignedShort(); } + @Nullable public Cue build() { if (planeWidth == 0 || planeHeight == 0 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 d701f99d73..e305259cbc 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text.ssa; +import androidx.annotation.Nullable; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; @@ -50,7 +51,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { private int formatTextIndex; public SsaDecoder() { - this(null); + this(/* initializationData= */ null); } /** @@ -59,7 +60,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * format line. The second must contain an SSA header that will be assumed common to all * samples. */ - public SsaDecoder(List initializationData) { + public SsaDecoder(@Nullable List initializationData) { super("SsaDecoder"); if (initializationData != null && !initializationData.isEmpty()) { haveInitializationData = true; @@ -202,7 +203,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { cues.add(new Cue(text)); cueTimesUs.add(startTimeUs); if (endTimeUs != C.TIME_UNSET) { - cues.add(null); + cues.add(Cue.EMPTY); cueTimesUs.add(endTimeUs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java index 339119ed6b..9a3756194f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java @@ -32,7 +32,7 @@ import java.util.List; private final long[] cueTimesUs; /** - * @param cues The cues in the subtitle. Null entries may be used to represent empty cues. + * @param cues The cues in the subtitle. * @param cueTimesUs The cue times, in microseconds. */ public SsaSubtitle(Cue[] cues, long[] cueTimesUs) { @@ -61,7 +61,7 @@ import java.util.List; @Override public List getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); - if (index == -1 || cues[index] == null) { + if (index == -1 || cues[index] == Cue.EMPTY) { // timeUs is earlier than the start of the first cue, or we have an empty cue. return Collections.emptyList(); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index cf174283ec..eb2b704bee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -112,11 +112,13 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { // Read and parse the text and tags. textBuilder.setLength(0); tags.clear(); - while (!TextUtils.isEmpty(currentLine = subripData.readLine())) { + currentLine = subripData.readLine(); + while (!TextUtils.isEmpty(currentLine)) { if (textBuilder.length() > 0) { textBuilder.append("
        "); } textBuilder.append(processLine(currentLine, tags)); + currentLine = subripData.readLine(); } Spanned text = Html.fromHtml(textBuilder.toString()); @@ -133,7 +135,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { cues.add(buildCue(text, alignmentTag)); if (haveEndTimecode) { - cues.add(null); + cues.add(Cue.EMPTY); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java index a79df478e5..01ed1711a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java @@ -32,7 +32,7 @@ import java.util.List; private final long[] cueTimesUs; /** - * @param cues The cues in the subtitle. Null entries may be used to represent empty cues. + * @param cues The cues in the subtitle. * @param cueTimesUs The cue times, in microseconds. */ public SubripSubtitle(Cue[] cues, long[] cueTimesUs) { @@ -61,7 +61,7 @@ import java.util.List; @Override public List getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); - if (index == -1 || cues[index] == null) { + if (index == -1 || cues[index] == Cue.EMPTY) { // timeUs is earlier than the start of the first cue, or we have an empty cue. return Collections.emptyList(); } else { 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 89017a40c0..c8f2979c58 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 @@ -65,6 +65,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { private static final float DEFAULT_VERTICAL_PLACEMENT = 0.85f; private final ParsableByteArray parsableByteArray; + private boolean customVerticalPlacement; private int defaultFontFace; private int defaultColorRgba; @@ -80,10 +81,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { public Tx3gDecoder(List initializationData) { super("Tx3gDecoder"); parsableByteArray = new ParsableByteArray(); - decodeInitializationData(initializationData); - } - private void decodeInitializationData(List initializationData) { if (initializationData != null && initializationData.size() == 1 && (initializationData.get(0).length == 48 || initializationData.get(0).length == 53)) { byte[] initializationBytes = initializationData.get(0); @@ -151,8 +149,16 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { } parsableByteArray.setPosition(position + atomSize); } - return new Tx3gSubtitle(new Cue(cueText, null, verticalPlacement, Cue.LINE_TYPE_FRACTION, - Cue.ANCHOR_TYPE_START, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET)); + return new Tx3gSubtitle( + new Cue( + cueText, + /* textAlignment= */ null, + verticalPlacement, + Cue.LINE_TYPE_FRACTION, + Cue.ANCHOR_TYPE_START, + Cue.DIMEN_UNSET, + Cue.TYPE_UNSET, + Cue.DIMEN_UNSET)); } private static String readSubtitleText(ParsableByteArray parsableByteArray) From fbb76243bdd4c910a1bf2ca725d50cb0269033b4 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jul 2019 20:02:32 +0100 Subject: [PATCH 1420/1556] Clean up DRM post requests - Explicitly specify HTTP_METHOD_POST (previously this was implicit as a result of the body data being non-null) - Use null when there's no body data (it's converted to null inside of the DataSpec constructor anyway) PiperOrigin-RevId: 256573384 --- .../android/exoplayer2/drm/HttpMediaDrmCallback.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 a3e602e404..23b2300dfa 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 @@ -111,7 +111,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { String url = request.getDefaultUrl() + "&signedRequest=" + Util.fromUtf8Bytes(request.getData()); - return executePost(dataSourceFactory, url, Util.EMPTY_BYTE_ARRAY, null); + return executePost(dataSourceFactory, url, /* httpBody= */ null, /* requestProperties= */ null); } @Override @@ -139,7 +139,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { private static byte[] executePost( HttpDataSource.Factory dataSourceFactory, String url, - byte[] data, + @Nullable byte[] httpBody, @Nullable Map requestProperties) throws IOException { HttpDataSource dataSource = dataSourceFactory.createDataSource(); @@ -154,7 +154,8 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { DataSpec dataSpec = new DataSpec( Uri.parse(url), - data, + DataSpec.HTTP_METHOD_POST, + httpBody, /* absoluteStreamPosition= */ 0, /* position= */ 0, /* length= */ C.LENGTH_UNSET, From d66f0c51a4e6789430cff22194996d2a859b5900 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 5 Jul 2019 16:20:17 +0100 Subject: [PATCH 1421/1556] CEA608: no-op readability clean-up PiperOrigin-RevId: 256676196 --- .../exoplayer2/text/cea/Cea608Decoder.java | 87 +++++++++++-------- 1 file changed, 49 insertions(+), 38 deletions(-) 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 774b94a43c..9d4b914d76 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 @@ -387,45 +387,27 @@ public final class Cea608Decoder extends CeaDecoder { continue; } - // Special North American character set. - // ccData1 - 0|0|0|1|C|0|0|1 - // ccData2 - 0|0|1|1|X|X|X|X - if (((ccData1 & 0xF7) == 0x11) && ((ccData2 & 0xF0) == 0x30)) { - if (getChannel(ccData1) == selectedChannel) { + if (!updateAndVerifyCurrentChannel(ccData1)) { + // Wrong channel. + continue; + } + + if (isCtrlCode(ccData1)) { + if (isSpecialChar(ccData1, ccData2)) { + // Special North American character. currentCueBuilder.append(getSpecialChar(ccData2)); - } - continue; - } - - // Extended Western European character set. - // ccData1 - 0|0|0|1|C|0|1|S - // ccData2 - 0|0|1|X|X|X|X|X - if (((ccData1 & 0xF6) == 0x12) && (ccData2 & 0xE0) == 0x20) { - if (getChannel(ccData1) == selectedChannel) { - // Remove standard equivalent of the special extended char before appending new one + } else if (isExtendedWestEuropeanChar(ccData1, ccData2)) { + // Extended West European character. + // Remove standard equivalent of the special extended char before appending new one. currentCueBuilder.backspace(); - if ((ccData1 & 0x01) == 0x00) { - // Extended Spanish/Miscellaneous and French character set (S = 0). - currentCueBuilder.append(getExtendedEsFrChar(ccData2)); - } else { - // Extended Portuguese and German/Danish character set (S = 1). - currentCueBuilder.append(getExtendedPtDeChar(ccData2)); - } + currentCueBuilder.append(getExtendedWestEuropeanChar(ccData1, ccData2)); + } else { + // Non-character control code. + handleCtrl(ccData1, ccData2, repeatedControlPossible); } continue; } - // Control character. - // ccData1 - 0|0|0|X|X|X|X|X - if ((ccData1 & 0xE0) == 0x00) { - handleCtrl(ccData1, ccData2, repeatedControlPossible); - continue; - } - - if (currentChannel != selectedChannel) { - continue; - } - // Basic North American character set. currentCueBuilder.append(getChar(ccData1)); if ((ccData2 & 0xE0) != 0x00) { @@ -440,8 +422,14 @@ public final class Cea608Decoder extends CeaDecoder { } } + private boolean updateAndVerifyCurrentChannel(byte cc1) { + if (isCtrlCode(cc1)) { + currentChannel = getChannel(cc1); + } + return currentChannel == selectedChannel; + } + private void handleCtrl(byte cc1, byte cc2, boolean repeatedControlPossible) { - currentChannel = getChannel(cc1); // Most control commands are sent twice in succession to ensure they are received properly. We // don't want to process duplicate commands, so if we see the same repeatable command twice in a // row then we ignore the second one. @@ -459,10 +447,6 @@ public final class Cea608Decoder extends CeaDecoder { } } - if (currentChannel != selectedChannel) { - return; - } - if (isMidrowCtrlCode(cc1, cc2)) { handleMidrowCtrl(cc2); } else if (isPreambleAddressCode(cc1, cc2)) { @@ -681,11 +665,33 @@ public final class Cea608Decoder extends CeaDecoder { return (char) BASIC_CHARACTER_SET[index]; } + private static boolean isSpecialChar(byte cc1, byte cc2) { + // cc1 - 0|0|0|1|C|0|0|1 + // cc2 - 0|0|1|1|X|X|X|X + return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x30); + } + private static char getSpecialChar(byte ccData) { int index = ccData & 0x0F; return (char) SPECIAL_CHARACTER_SET[index]; } + private static boolean isExtendedWestEuropeanChar(byte cc1, byte cc2) { + // cc1 - 0|0|0|1|C|0|1|S + // cc2 - 0|0|1|X|X|X|X|X + return ((cc1 & 0xF6) == 0x12) && ((cc2 & 0xE0) == 0x20); + } + + private static char getExtendedWestEuropeanChar(byte cc1, byte cc2) { + if ((cc1 & 0x01) == 0x00) { + // Extended Spanish/Miscellaneous and French character set (S = 0). + return getExtendedEsFrChar(cc2); + } else { + // Extended Portuguese and German/Danish character set (S = 1). + return getExtendedPtDeChar(cc2); + } + } + private static char getExtendedEsFrChar(byte ccData) { int index = ccData & 0x1F; return (char) SPECIAL_ES_FR_CHARACTER_SET[index]; @@ -696,6 +702,11 @@ public final class Cea608Decoder extends CeaDecoder { return (char) SPECIAL_PT_DE_CHARACTER_SET[index]; } + private static boolean isCtrlCode(byte cc1) { + // cc1 - 0|0|0|X|X|X|X|X + return (cc1 & 0xE0) == 0x00; + } + private static int getChannel(byte cc1) { // cc1 - X|X|X|X|C|X|X|X return (cc1 >> 3) & 0x1; From e852ff1dfa1cc1b2339773c47d406c7506507040 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 5 Jul 2019 17:07:38 +0100 Subject: [PATCH 1422/1556] Add Nullable annotations to CastPlayer PiperOrigin-RevId: 256680382 --- .../exoplayer2/ext/cast/CastPlayer.java | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index db6f71286e..03518ac18a 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -83,8 +83,6 @@ public final class CastPlayer extends BasePlayer { private final CastTimelineTracker timelineTracker; private final Timeline.Period period; - private RemoteMediaClient remoteMediaClient; - // Result callbacks. private final StatusListener statusListener; private final SeekResultCallback seekResultCallback; @@ -93,9 +91,10 @@ public final class CastPlayer extends BasePlayer { private final CopyOnWriteArrayList listeners; private final ArrayList notificationsBatch; private final ArrayDeque ongoingNotificationsTasks; - private SessionAvailabilityListener sessionAvailabilityListener; + @Nullable private SessionAvailabilityListener sessionAvailabilityListener; // Internal state. + @Nullable private RemoteMediaClient remoteMediaClient; private CastTimeline currentTimeline; private TrackGroupArray currentTrackGroups; private TrackSelectionArray currentTrackSelection; @@ -148,6 +147,7 @@ public final class CastPlayer extends BasePlayer { * starts at position 0. * @return The Cast {@code PendingResult}, or null if no session is available. */ + @Nullable public PendingResult loadItem(MediaQueueItem item, long positionMs) { return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF); } @@ -163,8 +163,9 @@ public final class CastPlayer extends BasePlayer { * @param repeatMode The repeat mode for the created media queue. * @return The Cast {@code PendingResult}, or null if no session is available. */ - public PendingResult loadItems(MediaQueueItem[] items, int startIndex, - long positionMs, @RepeatMode int repeatMode) { + @Nullable + public PendingResult loadItems( + MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) { if (remoteMediaClient != null) { positionMs = positionMs != C.TIME_UNSET ? positionMs : 0; waitingForInitialTimeline = true; @@ -180,6 +181,7 @@ public final class CastPlayer extends BasePlayer { * @param items The items to append. * @return The Cast {@code PendingResult}, or null if no media queue exists. */ + @Nullable public PendingResult addItems(MediaQueueItem... items) { return addItems(MediaQueueItem.INVALID_ITEM_ID, items); } @@ -194,6 +196,7 @@ public final class CastPlayer extends BasePlayer { * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code * periodId} exist. */ + @Nullable public PendingResult addItems(int periodId, MediaQueueItem... items) { if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID || currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) { @@ -211,6 +214,7 @@ public final class CastPlayer extends BasePlayer { * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code * periodId} exist. */ + @Nullable public PendingResult removeItem(int periodId) { if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { return remoteMediaClient.queueRemoveItem(periodId, null); @@ -229,6 +233,7 @@ public final class CastPlayer extends BasePlayer { * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code * periodId} exist. */ + @Nullable public PendingResult moveItem(int periodId, int newIndex) { Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount()); if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { @@ -246,6 +251,7 @@ public final class CastPlayer extends BasePlayer { * @return The item that corresponds to the period with the given id, or null if no media queue or * period with id {@code periodId} exist. */ + @Nullable public MediaQueueItem getItem(int periodId) { MediaStatus mediaStatus = getMediaStatus(); return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET @@ -264,9 +270,9 @@ public final class CastPlayer extends BasePlayer { /** * Sets a listener for updates on the cast session availability. * - * @param listener The {@link SessionAvailabilityListener}. + * @param listener The {@link SessionAvailabilityListener}, or null to clear the listener. */ - public void setSessionAvailabilityListener(SessionAvailabilityListener listener) { + public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) { sessionAvailabilityListener = listener; } @@ -323,6 +329,7 @@ public final class CastPlayer extends BasePlayer { } @Override + @Nullable public ExoPlaybackException getPlaybackError() { return null; } @@ -530,7 +537,7 @@ public final class CastPlayer extends BasePlayer { // Internal methods. - public void updateInternalState() { + private void updateInternalState() { if (remoteMediaClient == null) { // There is no session. We leave the state of the player as it is now. return; @@ -676,7 +683,8 @@ public final class CastPlayer extends BasePlayer { } } - private @Nullable MediaStatus getMediaStatus() { + @Nullable + private MediaStatus getMediaStatus() { return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null; } From ecd88c71d2ee5beb08a553091a418fb2e442b87b Mon Sep 17 00:00:00 2001 From: olly Date: Sat, 6 Jul 2019 11:09:36 +0100 Subject: [PATCH 1423/1556] Remove some UI classes from nullness blacklist PiperOrigin-RevId: 256751627 --- .../exoplayer2/ui/AspectRatioFrameLayout.java | 14 ++++++++------ .../android/exoplayer2/ui/PlayerControlView.java | 11 +++++++---- .../android/exoplayer2/ui/SubtitleView.java | 15 ++++++++++----- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index d4a37ea4ef..268219b6d5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.ui; import android.content.Context; import android.content.res.TypedArray; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import android.util.AttributeSet; import android.widget.FrameLayout; import java.lang.annotation.Documented; @@ -97,16 +98,16 @@ public final class AspectRatioFrameLayout extends FrameLayout { private final AspectRatioUpdateDispatcher aspectRatioUpdateDispatcher; - private AspectRatioListener aspectRatioListener; + @Nullable private AspectRatioListener aspectRatioListener; private float videoAspectRatio; - private @ResizeMode int resizeMode; + @ResizeMode private int resizeMode; public AspectRatioFrameLayout(Context context) { - this(context, null); + this(context, /* attrs= */ null); } - public AspectRatioFrameLayout(Context context, AttributeSet attrs) { + public AspectRatioFrameLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); resizeMode = RESIZE_MODE_FIT; if (attrs != null) { @@ -136,9 +137,10 @@ public final class AspectRatioFrameLayout extends FrameLayout { /** * Sets the {@link AspectRatioListener}. * - * @param listener The listener to be notified about aspect ratios changes. + * @param listener The listener to be notified about aspect ratios changes, or null to clear a + * listener that was previously set. */ - public void setAspectRatioListener(AspectRatioListener listener) { + public void setAspectRatioListener(@Nullable AspectRatioListener listener) { this.aspectRatioListener = listener; } 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 b9b2456722..bba422e488 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 @@ -281,19 +281,22 @@ public class PlayerControlView extends FrameLayout { private long currentWindowOffset; public PlayerControlView(Context context) { - this(context, null); + this(context, /* attrs= */ null); } - public PlayerControlView(Context context, AttributeSet attrs) { + public PlayerControlView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } - public PlayerControlView(Context context, AttributeSet attrs, int defStyleAttr) { + public PlayerControlView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, attrs); } public PlayerControlView( - Context context, AttributeSet attrs, int defStyleAttr, AttributeSet playbackAttrs) { + Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, + @Nullable AttributeSet playbackAttrs) { super(context, attrs, defStyleAttr); int controllerLayoutId = R.layout.exo_player_control_view; rewindMs = DEFAULT_REWIND_MS; 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 5d99eda109..0bdc1acc88 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 @@ -53,8 +53,8 @@ public final class SubtitleView extends View implements TextOutput { private final List painters; - private List cues; - private @Cue.TextSizeType int textSizeType; + @Nullable private List cues; + @Cue.TextSizeType private int textSizeType; private float textSize; private boolean applyEmbeddedStyles; private boolean applyEmbeddedFontSizes; @@ -62,10 +62,10 @@ public final class SubtitleView extends View implements TextOutput { private float bottomPaddingFraction; public SubtitleView(Context context) { - this(context, null); + this(context, /* attrs= */ null); } - public SubtitleView(Context context, AttributeSet attrs) { + public SubtitleView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); painters = new ArrayList<>(); textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL; @@ -246,7 +246,11 @@ public final class SubtitleView extends View implements TextOutput { @Override public void dispatchDraw(Canvas canvas) { - int cueCount = (cues == null) ? 0 : cues.size(); + List cues = this.cues; + if (cues == null || cues.isEmpty()) { + return; + } + int rawViewHeight = getHeight(); // Calculate the cue box bounds relative to the canvas after padding is taken into account. @@ -267,6 +271,7 @@ public final class SubtitleView extends View implements TextOutput { return; } + int cueCount = cues.size(); for (int i = 0; i < cueCount; i++) { Cue cue = cues.get(i); float cueTextSizePx = resolveCueTextSize(cue, rawViewHeight, viewHeightMinusPadding); From e3af045adbc7c385ad1ce6a97bc35befc3e009c2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 8 Jul 2019 17:24:18 +0100 Subject: [PATCH 1424/1556] CEA608: Fix repeated Special North American chars. We currently handle most the control code logic after handling special characters. This includes filtering out repeated control codes and checking for the correct channel. As the special character sets are control codes as well, these checks should happen before parsing the characters. Issue:#6133 PiperOrigin-RevId: 256993672 --- RELEASENOTES.md | 2 + .../exoplayer2/text/cea/Cea608Decoder.java | 91 +++++++++---------- 2 files changed, 45 insertions(+), 48 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 855b0e94f2..fd748d45ab 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,8 @@ * SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. * FLV: Fix bug that caused playback of some live streams to not start ([#6111](https://github.com/google/ExoPlayer/issues/6111)). +* CEA608: Fix repetition of special North American characters + ([#6133](https://github.com/google/ExoPlayer/issues/6133)). ### 2.10.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 9d4b914d76..5a14063aa1 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 @@ -242,7 +242,7 @@ public final class Cea608Decoder extends CeaDecoder { private int captionMode; private int captionRowCount; - private boolean captionValid; + private boolean isCaptionValid; private boolean repeatableControlSet; private byte repeatableControlCc1; private byte repeatableControlCc2; @@ -300,7 +300,7 @@ public final class Cea608Decoder extends CeaDecoder { setCaptionMode(CC_MODE_UNKNOWN); setCaptionRowCount(DEFAULT_CAPTIONS_ROW_COUNT); resetCueBuilders(); - captionValid = false; + isCaptionValid = false; repeatableControlSet = false; repeatableControlCc1 = 0; repeatableControlCc2 = 0; @@ -358,13 +358,19 @@ public final class Cea608Decoder extends CeaDecoder { continue; } - boolean repeatedControlPossible = repeatableControlSet; - repeatableControlSet = false; + boolean previousIsCaptionValid = isCaptionValid; + isCaptionValid = + (ccHeader & CC_VALID_FLAG) == CC_VALID_FLAG + && ODD_PARITY_BYTE_TABLE[ccByte1] + && ODD_PARITY_BYTE_TABLE[ccByte2]; - boolean previousCaptionValid = captionValid; - captionValid = (ccHeader & CC_VALID_FLAG) == CC_VALID_FLAG; - if (!captionValid) { - if (previousCaptionValid) { + if (isRepeatedCommand(isCaptionValid, ccData1, ccData2)) { + // Ignore repeated valid commands. + continue; + } + + if (!isCaptionValid) { + if (previousIsCaptionValid) { // The encoder has flipped the validity bit to indicate captions are being turned off. resetCueBuilders(); captionDataProcessed = true; @@ -372,15 +378,6 @@ public final class Cea608Decoder extends CeaDecoder { continue; } - // If we've reached this point then there is data to process; flag that work has been done. - captionDataProcessed = true; - - if (!ODD_PARITY_BYTE_TABLE[ccByte1] || !ODD_PARITY_BYTE_TABLE[ccByte2]) { - // The data is invalid. - resetCueBuilders(); - continue; - } - maybeUpdateIsInCaptionService(ccData1, ccData2); if (!isInCaptionService) { // Only the Captioning service is supported. Drop all other bytes. @@ -393,26 +390,29 @@ public final class Cea608Decoder extends CeaDecoder { } if (isCtrlCode(ccData1)) { - if (isSpecialChar(ccData1, ccData2)) { - // Special North American character. - currentCueBuilder.append(getSpecialChar(ccData2)); + if (isSpecialNorthAmericanChar(ccData1, ccData2)) { + currentCueBuilder.append(getSpecialNorthAmericanChar(ccData2)); } else if (isExtendedWestEuropeanChar(ccData1, ccData2)) { - // Extended West European character. // Remove standard equivalent of the special extended char before appending new one. currentCueBuilder.backspace(); currentCueBuilder.append(getExtendedWestEuropeanChar(ccData1, ccData2)); - } else { - // Non-character control code. - handleCtrl(ccData1, ccData2, repeatedControlPossible); + } else if (isMidrowCtrlCode(ccData1, ccData2)) { + handleMidrowCtrl(ccData2); + } else if (isPreambleAddressCode(ccData1, ccData2)) { + handlePreambleAddressCode(ccData1, ccData2); + } else if (isTabCtrlCode(ccData1, ccData2)) { + currentCueBuilder.tabOffset = ccData2 - 0x20; + } else if (isMiscCode(ccData1, ccData2)) { + handleMiscCode(ccData2); + } + } else { + // Basic North American character set. + currentCueBuilder.append(getBasicChar(ccData1)); + if ((ccData2 & 0xE0) != 0x00) { + currentCueBuilder.append(getBasicChar(ccData2)); } - continue; - } - - // Basic North American character set. - currentCueBuilder.append(getChar(ccData1)); - if ((ccData2 & 0xE0) != 0x00) { - currentCueBuilder.append(getChar(ccData2)); } + captionDataProcessed = true; } if (captionDataProcessed) { @@ -429,14 +429,15 @@ public final class Cea608Decoder extends CeaDecoder { return currentChannel == selectedChannel; } - private void handleCtrl(byte cc1, byte cc2, boolean repeatedControlPossible) { + private boolean isRepeatedCommand(boolean captionValid, byte cc1, byte cc2) { // Most control commands are sent twice in succession to ensure they are received properly. We // don't want to process duplicate commands, so if we see the same repeatable command twice in a // row then we ignore the second one. - if (isRepeatable(cc1)) { - if (repeatedControlPossible && repeatableControlCc1 == cc1 && repeatableControlCc2 == cc2) { + if (captionValid && isRepeatable(cc1)) { + if (repeatableControlSet && repeatableControlCc1 == cc1 && repeatableControlCc2 == cc2) { // This is a repeated command, so we ignore it. - return; + repeatableControlSet = false; + return true; } else { // This is the first occurrence of a repeatable command. Set the repeatable control // variables so that we can recognize and ignore a duplicate (if there is one), and then @@ -445,17 +446,11 @@ public final class Cea608Decoder extends CeaDecoder { repeatableControlCc1 = cc1; repeatableControlCc2 = cc2; } + } else { + // This command is not repeatable. + repeatableControlSet = false; } - - if (isMidrowCtrlCode(cc1, cc2)) { - handleMidrowCtrl(cc2); - } else if (isPreambleAddressCode(cc1, cc2)) { - handlePreambleAddressCode(cc1, cc2); - } else if (isTabCtrlCode(cc1, cc2)) { - currentCueBuilder.tabOffset = cc2 - 0x20; - } else if (isMiscCode(cc1, cc2)) { - handleMiscCode(cc2); - } + return false; } private void handleMidrowCtrl(byte cc2) { @@ -660,18 +655,18 @@ public final class Cea608Decoder extends CeaDecoder { } } - private static char getChar(byte ccData) { + private static char getBasicChar(byte ccData) { int index = (ccData & 0x7F) - 0x20; return (char) BASIC_CHARACTER_SET[index]; } - private static boolean isSpecialChar(byte cc1, byte cc2) { + private static boolean isSpecialNorthAmericanChar(byte cc1, byte cc2) { // cc1 - 0|0|0|1|C|0|0|1 // cc2 - 0|0|1|1|X|X|X|X return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x30); } - private static char getSpecialChar(byte ccData) { + private static char getSpecialNorthAmericanChar(byte ccData) { int index = ccData & 0x0F; return (char) SPECIAL_CHARACTER_SET[index]; } From 92fb654ab6c3ee3af1ef7992b36f10e23e7733a4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 9 Jul 2019 10:11:26 +0100 Subject: [PATCH 1425/1556] Update Robolectric to stable version 4.3. We currently use an alpha version which allowed us to access new threading features. The stable version of this has been released now and we can switch back. PiperOrigin-RevId: 257149681 --- constants.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.gradle b/constants.gradle index 3fe22a2762..d9770415f9 100644 --- a/constants.gradle +++ b/constants.gradle @@ -20,7 +20,7 @@ project.ext { compileSdkVersion = 28 dexmakerVersion = '2.21.0' mockitoVersion = '2.25.0' - robolectricVersion = '4.3-alpha-2' + robolectricVersion = '4.3' autoValueVersion = '1.6' autoServiceVersion = '1.0-rc4' checkerframeworkVersion = '2.5.0' From b3d258b6cf5953acf643688390157b1efae805a2 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 9 Jul 2019 11:18:50 +0100 Subject: [PATCH 1426/1556] Fix race condition in DownloadHelper Sending MESSAGE_PREPARE_SOURCE should happen last in the constructor. It was previously happening before initialization finished (and in particular before pendingMediaPeriods was instantiated). Issue: #6146 PiperOrigin-RevId: 257158275 --- .../google/android/exoplayer2/offline/DownloadHelper.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index e7cf87ed6e..4858eec6b7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -809,10 +809,10 @@ public final class DownloadHelper { private final MediaSource mediaSource; private final DownloadHelper downloadHelper; private final Allocator allocator; + private final ArrayList pendingMediaPeriods; + private final Handler downloadHelperHandler; private final HandlerThread mediaSourceThread; private final Handler mediaSourceHandler; - private final Handler downloadHelperHandler; - private final ArrayList pendingMediaPeriods; @Nullable public Object manifest; public @MonotonicNonNull Timeline timeline; @@ -824,6 +824,7 @@ public final class DownloadHelper { this.mediaSource = mediaSource; this.downloadHelper = downloadHelper; allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); + pendingMediaPeriods = new ArrayList<>(); @SuppressWarnings("methodref.receiver.bound.invalid") Handler downloadThreadHandler = Util.createHandler(this::handleDownloadHelperCallbackMessage); this.downloadHelperHandler = downloadThreadHandler; @@ -831,7 +832,6 @@ public final class DownloadHelper { mediaSourceThread.start(); mediaSourceHandler = Util.createHandler(mediaSourceThread.getLooper(), /* callback= */ this); mediaSourceHandler.sendEmptyMessage(MESSAGE_PREPARE_SOURCE); - pendingMediaPeriods = new ArrayList<>(); } public void release() { From 65d9c11027a1321eb3d59aabfc93e10b0750822d Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 9 Jul 2019 11:50:56 +0100 Subject: [PATCH 1427/1556] Bump version to 2.10.3 PiperOrigin-RevId: 257161518 --- RELEASENOTES.md | 27 ++++++++++++------- constants.gradle | 4 +-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 ++--- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fd748d45ab..5a1afdd559 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,27 +6,34 @@ and analytics reporting (TODO: link to developer guide page/blog post). * Add basic DRM support to the Cast demo app. * Offline: Add `Scheduler` implementation that uses `WorkManager`. -* Display last frame when seeking to end of stream - ([#2568](https://github.com/google/ExoPlayer/issues/2568)). * Assume that encrypted content requires secure decoders in renderer support checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Decoders: Prefer decoders that advertise format support over ones that do not, even if they are listed lower in the `MediaCodecList`. -* Audio: - * Fix an issue where not all audio was played out when the configuration - for the underlying track was changing (e.g., at some period transitions). - * Fix an issue where playback speed was applied inaccurately in playlists - ([#6117](https://github.com/google/ExoPlayer/issues/6117)). * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). * Add VR player demo. * Wrap decoder exceptions in a new `DecoderException` class and report as renderer error. -* SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. -* FLV: Fix bug that caused playback of some live streams to not start - ([#6111](https://github.com/google/ExoPlayer/issues/6111)). + +### 2.10.3 ### + +* Display last frame when seeking to end of stream + ([#2568](https://github.com/google/ExoPlayer/issues/2568)). +* Audio: + * Fix an issue where not all audio was played out when the configuration + for the underlying track was changing (e.g., at some period transitions). + * Fix an issue where playback speed was applied inaccurately in playlists + ([#6117](https://github.com/google/ExoPlayer/issues/6117)). +* UI: Fix `PlayerView` incorrectly consuming touch events if no controller is + attached ([#6109](https://github.com/google/ExoPlayer/issues/6133)). * CEA608: Fix repetition of special North American characters ([#6133](https://github.com/google/ExoPlayer/issues/6133)). +* FLV: Fix bug that caused playback of some live streams to not start + ([#6111](https://github.com/google/ExoPlayer/issues/6111)). +* SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. +* MediaSession extension: Fix `MediaSessionConnector.play()` not resuming + playback ([#6093](https://github.com/google/ExoPlayer/issues/6093)). ### 2.10.2 ### diff --git a/constants.gradle b/constants.gradle index d9770415f9..e857d5a812 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.10.2' - releaseVersionCode = 2010002 + releaseVersion = '2.10.3' + releaseVersionCode = 2010003 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 28 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 db3f3943e1..190f4de5a6 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.10.2"; + public static final String VERSION = "2.10.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.10.2"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.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 = 2010002; + public static final int VERSION_INT = 2010003; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 77e1e4cc1e3a90219bc1f1e128f4d74c7f7a9700 Mon Sep 17 00:00:00 2001 From: "Venkatarama NG. Avadhani" Date: Tue, 9 Jul 2019 12:17:54 +0530 Subject: [PATCH 1428/1556] Add vorbis comments support to flac extractor Decode and add vorbis comments from the flac file to metadata. #5527 --- .../exoplayer2/ext/flac/FlacDecoderJni.java | 10 ++ .../exoplayer2/ext/flac/FlacExtractor.java | 23 +++- extensions/flac/src/main/jni/flac_jni.cc | 26 +++++ extensions/flac/src/main/jni/flac_parser.cc | 28 +++++ .../flac/src/main/jni/include/flac_parser.h | 14 +++ .../metadata/vorbis/VorbisCommentDecoder.java | 59 ++++++++++ .../metadata/vorbis/VorbisCommentFrame.java | 102 ++++++++++++++++++ .../vorbis/VorbisCommentDecoderTest.java | 90 ++++++++++++++++ .../vorbis/VorbisCommentFrameTest.java | 43 ++++++++ 9 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrameTest.java diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java index 32ef22dab0..448e2a1b05 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; /** * JNI wrapper for the libflac Flac decoder. @@ -151,6 +152,12 @@ import java.nio.ByteBuffer; return streamInfo; } + /** Decodes and consumes the Vorbis Comment section from the FLAC stream. */ + @Nullable + public ArrayList decodeVorbisComment() throws IOException, InterruptedException { + return flacDecodeVorbisComment(nativeDecoderContext); + } + /** * Decodes and consumes the next frame from the FLAC stream into the given byte buffer. If any IO * error occurs, resets the stream and input to the given {@code retryPosition}. @@ -269,6 +276,9 @@ import java.nio.ByteBuffer; private native FlacStreamInfo flacDecodeMetadata(long context) throws IOException, InterruptedException; + private native ArrayList flacDecodeVorbisComment(long context) + throws IOException, InterruptedException; + private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) throws IOException, InterruptedException; diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 082068f34d..307cdfa8c8 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import com.google.android.exoplayer2.metadata.vorbis.VorbisCommentDecoder; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.MimeTypes; @@ -42,6 +43,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Arrays; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -91,6 +93,7 @@ public final class FlacExtractor implements Extractor { private @MonotonicNonNull OutputFrameHolder outputFrameHolder; @Nullable private Metadata id3Metadata; + @Nullable private Metadata vorbisMetadata; @Nullable private FlacBinarySearchSeeker binarySearchSeeker; /** Constructs an instance with flags = 0. */ @@ -224,11 +227,16 @@ public final class FlacExtractor implements Extractor { } streamInfoDecoded = true; + vorbisMetadata = decodeVorbisComment(input); if (this.streamInfo == null) { this.streamInfo = streamInfo; binarySearchSeeker = outputSeekMap(decoderJni, streamInfo, input.getLength(), extractorOutput); - outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata, trackOutput); + Metadata metadata = id3MetadataDisabled ? null : id3Metadata; + if (vorbisMetadata != null) { + metadata = vorbisMetadata.copyWithAppendedEntriesFrom(metadata); + } + outputFormat(streamInfo, metadata, trackOutput); outputBuffer.reset(streamInfo.maxDecodedFrameSize()); outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); } @@ -262,6 +270,19 @@ public final class FlacExtractor implements Extractor { return Arrays.equals(header, FLAC_SIGNATURE); } + @Nullable + private Metadata decodeVorbisComment(ExtractorInput input) + throws InterruptedException, IOException { + try { + ArrayList vorbisCommentList = decoderJni.decodeVorbisComment(); + return new VorbisCommentDecoder().decodeVorbisComments(vorbisCommentList); + } catch (IOException e) { + decoderJni.reset(0); + input.setRetryPosition(0, e); + throw e; + } + } + /** * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to * handle seeks. diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 298719d48d..0971ba5883 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -110,6 +110,32 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { streamInfo.total_samples); } +DECODER_FUNC(jobject, flacDecodeVorbisComment, jlong jContext) { + Context *context = reinterpret_cast(jContext); + context->source->setFlacDecoderJni(env, thiz); + + VorbisComment vorbisComment = context->parser->getVorbisComment(); + + if (vorbisComment.numComments == 0) { + return NULL; + } else { + jclass java_util_ArrayList = env->FindClass("java/util/ArrayList"); + + jmethodID java_util_ArrayList_ = env->GetMethodID(java_util_ArrayList, "", "(I)V"); + jmethodID java_util_ArrayList_add = env->GetMethodID(java_util_ArrayList, "add", + "(Ljava/lang/Object;)Z"); + + jobject result = env->NewObject(java_util_ArrayList, java_util_ArrayList_, + vorbisComment.numComments); + for (FLAC__uint32 i = 0; i < vorbisComment.numComments; ++i) { + jstring element = env->NewStringUTF(vorbisComment.metadataArray[i]); + env->CallBooleanMethod(result, java_util_ArrayList_add, element); + env->DeleteLocalRef(element); + } + return result; + } +} + DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) { Context *context = reinterpret_cast(jContext); context->source->setFlacDecoderJni(env, thiz); diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index 83d3367415..06c98302fd 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -172,6 +172,30 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) { case FLAC__METADATA_TYPE_SEEKTABLE: mSeekTable = &metadata->data.seek_table; break; + case FLAC__METADATA_TYPE_VORBIS_COMMENT: + if (!mVorbisCommentValid) { + FLAC__uint32 count = 0; + const FLAC__StreamMetadata_VorbisComment *vc = + &metadata->data.vorbis_comment; + mVorbisCommentValid = true; + mVorbisComment.metadataArray = + (char **) malloc(vc->num_comments * sizeof(char *)); + for (FLAC__uint32 i = 0; i < vc->num_comments; ++i) { + FLAC__StreamMetadata_VorbisComment_Entry *vce = &vc->comments[i]; + if (vce->entry != NULL) { + mVorbisComment.metadataArray[count] = + (char *) malloc((vce->length + 1) * sizeof(char)); + memcpy(mVorbisComment.metadataArray[count], vce->entry, + vce->length); + mVorbisComment.metadataArray[count][vce->length] = '\0'; + count++; + } + } + mVorbisComment.numComments = count; + } else { + ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT"); + } + break; default: ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type); break; @@ -233,6 +257,7 @@ FLACParser::FLACParser(DataSource *source) mCurrentPos(0LL), mEOF(false), mStreamInfoValid(false), + mVorbisCommentValid(false), mWriteRequested(false), mWriteCompleted(false), mWriteBuffer(NULL), @@ -240,6 +265,7 @@ FLACParser::FLACParser(DataSource *source) ALOGV("FLACParser::FLACParser"); memset(&mStreamInfo, 0, sizeof(mStreamInfo)); memset(&mWriteHeader, 0, sizeof(mWriteHeader)); + memset(&mVorbisComment, 0, sizeof(mVorbisComment)); } FLACParser::~FLACParser() { @@ -266,6 +292,8 @@ bool FLACParser::init() { FLAC__METADATA_TYPE_STREAMINFO); FLAC__stream_decoder_set_metadata_respond(mDecoder, FLAC__METADATA_TYPE_SEEKTABLE); + FLAC__stream_decoder_set_metadata_respond(mDecoder, + FLAC__METADATA_TYPE_VORBIS_COMMENT); FLAC__StreamDecoderInitStatus initStatus; initStatus = FLAC__stream_decoder_init_stream( mDecoder, read_callback, seek_callback, tell_callback, length_callback, diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index cea7fbe33b..aec07d673e 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -26,6 +26,11 @@ typedef int status_t; +typedef struct VorbisComment_ { + int numComments; + char **metadataArray; +} VorbisComment; + class FLACParser { public: FLACParser(DataSource *source); @@ -71,6 +76,7 @@ class FLACParser { mEOF = false; if (newPosition == 0) { mStreamInfoValid = false; + mVorbisCommentValid = false; FLAC__stream_decoder_reset(mDecoder); } else { FLAC__stream_decoder_flush(mDecoder); @@ -96,6 +102,10 @@ class FLACParser { FLAC__STREAM_DECODER_END_OF_STREAM; } + VorbisComment getVorbisComment() { + return mVorbisComment; + } + private: DataSource *mDataSource; @@ -116,6 +126,8 @@ class FLACParser { const FLAC__StreamMetadata_SeekTable *mSeekTable; uint64_t firstFrameOffset; + bool mVorbisCommentValid; + // cached when a decoded PCM block is "written" by libFLAC parser bool mWriteRequested; bool mWriteCompleted; @@ -129,6 +141,8 @@ class FLACParser { FLACParser(const FLACParser &); FLACParser &operator=(const FLACParser &); + VorbisComment mVorbisComment; + // FLAC parser callbacks as C++ instance methods FLAC__StreamDecoderReadStatus readCallback(FLAC__byte buffer[], size_t *bytes); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java new file mode 100644 index 0000000000..a212532685 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2019 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.vorbis; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.metadata.Metadata; +import java.util.ArrayList; + +/** Decodes vorbis comments */ +public class VorbisCommentDecoder { + + private static final String SEPARATOR = "="; + + /** + * Decodes an {@link ArrayList} of vorbis comments. + * + * @param metadataStringList An {@link ArrayList} containing vorbis comments as {@link String} + * @return A {@link Metadata} structure with the vorbis comments as its entries. + */ + public Metadata decodeVorbisComments(@Nullable ArrayList metadataStringList) { + if (metadataStringList == null || metadataStringList.size() == 0) { + return null; + } + + ArrayList vorbisCommentFrames = new ArrayList<>(); + VorbisCommentFrame vorbisCommentFrame; + + for (String commentEntry : metadataStringList) { + String[] keyValue; + + keyValue = commentEntry.split(SEPARATOR); + if (keyValue.length != 2) { + /* Could not parse this comment, no key value pair found */ + continue; + } + vorbisCommentFrame = new VorbisCommentFrame(keyValue[0], keyValue[1]); + vorbisCommentFrames.add(vorbisCommentFrame); + } + + if (vorbisCommentFrames.size() > 0) { + return new Metadata(vorbisCommentFrames); + } else { + return null; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java new file mode 100644 index 0000000000..2deb5b1127 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2019 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.vorbis; + +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.metadata.Metadata; + +/** Base class for Vorbis Comment Frames. */ +public class VorbisCommentFrame implements Metadata.Entry { + + /** The frame key and value */ + public final String key; + + public final String value; + + /** + * @param key The key + * @param value Value corresponding to the key + */ + public VorbisCommentFrame(String key, String value) { + this.key = key; + this.value = value; + } + + /* package */ VorbisCommentFrame(Parcel in) { + this.key = castNonNull(in.readString()); + this.value = castNonNull(in.readString()); + } + + @Override + public String toString() { + return key; + } + + @Override + public int describeContents() { + return 0; + } + + // Parcelable implementation. + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(key); + dest.writeString(value); + } + + @Override + public boolean equals(@Nullable Object obj) { + if ((obj != null) && (obj.getClass() == this.getClass())) { + if (this == obj) { + return true; + } else { + VorbisCommentFrame compareFrame = (VorbisCommentFrame) obj; + if (this.key.equals(compareFrame.key) && this.value.equals(compareFrame.value)) { + return true; + } + } + } + return false; + } + + @Override + public int hashCode() { + int result = 17; + + result = 31 * result + key.hashCode(); + result = 31 * result + value.hashCode(); + + return result; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public VorbisCommentFrame createFromParcel(Parcel in) { + return new VorbisCommentFrame(in); + } + + @Override + public VorbisCommentFrame[] newArray(int size) { + return new VorbisCommentFrame[size]; + } + }; +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java new file mode 100644 index 0000000000..e2c2bcf021 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2019 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.vorbis; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.metadata.Metadata; +import java.util.ArrayList; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test for {@link VorbisCommentDecoder}. */ +@RunWith(AndroidJUnit4.class) +public final class VorbisCommentDecoderTest { + + @Test + public void decode() { + VorbisCommentDecoder decoder = new VorbisCommentDecoder(); + ArrayList commentsList = new ArrayList<>(); + + commentsList.add("Title=Test"); + commentsList.add("Artist=Test2"); + + Metadata metadata = decoder.decodeVorbisComments(commentsList); + + assertThat(metadata.length()).isEqualTo(2); + VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Title"); + assertThat(commentFrame.value).isEqualTo("Test"); + commentFrame = (VorbisCommentFrame) metadata.get(1); + assertThat(commentFrame.key).isEqualTo("Artist"); + assertThat(commentFrame.value).isEqualTo("Test2"); + } + + @Test + public void decodeEmptyList() { + VorbisCommentDecoder decoder = new VorbisCommentDecoder(); + ArrayList commentsList = new ArrayList<>(); + + Metadata metadata = decoder.decodeVorbisComments(commentsList); + + assertThat(metadata).isNull(); + } + + @Test + public void decodeTwoSeparators() { + VorbisCommentDecoder decoder = new VorbisCommentDecoder(); + ArrayList commentsList = new ArrayList<>(); + + commentsList.add("Title=Test"); + commentsList.add("Artist=Test=2"); + + Metadata metadata = decoder.decodeVorbisComments(commentsList); + + assertThat(metadata.length()).isEqualTo(1); + VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Title"); + assertThat(commentFrame.value).isEqualTo("Test"); + } + + @Test + public void decodeNoSeparators() { + VorbisCommentDecoder decoder = new VorbisCommentDecoder(); + ArrayList commentsList = new ArrayList<>(); + + commentsList.add("TitleTest"); + commentsList.add("Artist=Test2"); + + Metadata metadata = decoder.decodeVorbisComments(commentsList); + + assertThat(metadata.length()).isEqualTo(1); + VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Artist"); + assertThat(commentFrame.value).isEqualTo("Test2"); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrameTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrameTest.java new file mode 100644 index 0000000000..218de9649d --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrameTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2019 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.vorbis; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test for {@link VorbisCommentFrame}. */ +@RunWith(AndroidJUnit4.class) +public final class VorbisCommentFrameTest { + + @Test + public void testParcelable() { + VorbisCommentFrame vorbisCommentFrameToParcel = new VorbisCommentFrame("key", "value"); + + Parcel parcel = Parcel.obtain(); + vorbisCommentFrameToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + VorbisCommentFrame vorbisCommentFrameFromParcel = + VorbisCommentFrame.CREATOR.createFromParcel(parcel); + assertThat(vorbisCommentFrameFromParcel).isEqualTo(vorbisCommentFrameToParcel); + + parcel.recycle(); + } +} From 54abdfc85b413ae6884efb0122fdfe52bae9c1e1 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 9 Jul 2019 15:06:12 +0100 Subject: [PATCH 1429/1556] Fix syntax error in publish.gradle PiperOrigin-RevId: 257184313 --- publish.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/publish.gradle b/publish.gradle index 96ec3d2f10..f293673c49 100644 --- a/publish.gradle +++ b/publish.gradle @@ -31,7 +31,7 @@ if (project.ext.has("exoplayerPublishEnabled") task.doLast { task.outputs.files .filter { File file -> - file.path.contains("publications") + file.path.contains("publications") \ && file.name.matches("^pom-.+\\.xml\$") } .forEach { File file -> addLicense(file) } From 877923ce5f0cf3c33465d7558edff911c1e0ee11 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 9 Jul 2019 15:11:11 +0100 Subject: [PATCH 1430/1556] fix typo in release notes PiperOrigin-RevId: 257185017 --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5a1afdd559..e40bfe8d81 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -26,7 +26,7 @@ * Fix an issue where playback speed was applied inaccurately in playlists ([#6117](https://github.com/google/ExoPlayer/issues/6117)). * UI: Fix `PlayerView` incorrectly consuming touch events if no controller is - attached ([#6109](https://github.com/google/ExoPlayer/issues/6133)). + attached ([#6109](https://github.com/google/ExoPlayer/issues/6109)). * CEA608: Fix repetition of special North American characters ([#6133](https://github.com/google/ExoPlayer/issues/6133)). * FLV: Fix bug that caused playback of some live streams to not start From fb1f91b2a1b2e50d684e1df503c7962bcf05ce8b Mon Sep 17 00:00:00 2001 From: "Venkatarama NG. Avadhani" Date: Wed, 10 Jul 2019 17:23:01 +0530 Subject: [PATCH 1431/1556] Clean up vorbis comment extraction --- .../exoplayer2/ext/flac/FlacDecoderJni.java | 10 --- .../exoplayer2/ext/flac/FlacExtractor.java | 21 +---- extensions/flac/src/main/jni/flac_jni.cc | 61 +++++++------- extensions/flac/src/main/jni/flac_parser.cc | 25 ++---- .../flac/src/main/jni/include/flac_parser.h | 25 +++--- .../metadata/vorbis/VorbisCommentDecoder.java | 59 -------------- .../metadata/vorbis/VorbisCommentFrame.java | 5 +- .../exoplayer2/util/FlacStreamInfo.java | 81 +++++++++++++++++++ .../vorbis/VorbisCommentDecoderTest.java | 40 ++++----- 9 files changed, 158 insertions(+), 169 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java index 448e2a1b05..32ef22dab0 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java @@ -23,7 +23,6 @@ import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.ArrayList; /** * JNI wrapper for the libflac Flac decoder. @@ -152,12 +151,6 @@ import java.util.ArrayList; return streamInfo; } - /** Decodes and consumes the Vorbis Comment section from the FLAC stream. */ - @Nullable - public ArrayList decodeVorbisComment() throws IOException, InterruptedException { - return flacDecodeVorbisComment(nativeDecoderContext); - } - /** * Decodes and consumes the next frame from the FLAC stream into the given byte buffer. If any IO * error occurs, resets the stream and input to the given {@code retryPosition}. @@ -276,9 +269,6 @@ import java.util.ArrayList; private native FlacStreamInfo flacDecodeMetadata(long context) throws IOException, InterruptedException; - private native ArrayList flacDecodeVorbisComment(long context) - throws IOException, InterruptedException; - private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) throws IOException, InterruptedException; diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 307cdfa8c8..50e0458fd7 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -33,7 +33,6 @@ import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; -import com.google.android.exoplayer2.metadata.vorbis.VorbisCommentDecoder; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.MimeTypes; @@ -43,7 +42,6 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; -import java.util.ArrayList; import java.util.Arrays; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -93,7 +91,6 @@ public final class FlacExtractor implements Extractor { private @MonotonicNonNull OutputFrameHolder outputFrameHolder; @Nullable private Metadata id3Metadata; - @Nullable private Metadata vorbisMetadata; @Nullable private FlacBinarySearchSeeker binarySearchSeeker; /** Constructs an instance with flags = 0. */ @@ -227,14 +224,13 @@ public final class FlacExtractor implements Extractor { } streamInfoDecoded = true; - vorbisMetadata = decodeVorbisComment(input); if (this.streamInfo == null) { this.streamInfo = streamInfo; binarySearchSeeker = outputSeekMap(decoderJni, streamInfo, input.getLength(), extractorOutput); Metadata metadata = id3MetadataDisabled ? null : id3Metadata; - if (vorbisMetadata != null) { - metadata = vorbisMetadata.copyWithAppendedEntriesFrom(metadata); + if (streamInfo.vorbisComments != null) { + metadata = streamInfo.vorbisComments.copyWithAppendedEntriesFrom(metadata); } outputFormat(streamInfo, metadata, trackOutput); outputBuffer.reset(streamInfo.maxDecodedFrameSize()); @@ -270,19 +266,6 @@ public final class FlacExtractor implements Extractor { return Arrays.equals(header, FLAC_SIGNATURE); } - @Nullable - private Metadata decodeVorbisComment(ExtractorInput input) - throws InterruptedException, IOException { - try { - ArrayList vorbisCommentList = decoderJni.decodeVorbisComment(); - return new VorbisCommentDecoder().decodeVorbisComments(vorbisCommentList); - } catch (IOException e) { - decoderJni.reset(0); - input.setRetryPosition(0, e); - throw e; - } - } - /** * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to * handle seeks. diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 0971ba5883..600f181890 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -17,6 +17,7 @@ #include #include #include +#include #include "include/flac_parser.h" #define LOG_TAG "flac_jni" @@ -90,50 +91,48 @@ DECODER_FUNC(jlong, flacInit) { DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { Context *context = reinterpret_cast(jContext); + jobject commentArrayList = NULL; context->source->setFlacDecoderJni(env, thiz); if (!context->parser->decodeMetadata()) { return NULL; } + bool vorbisCommentValid = context->parser->isVorbisCommentValid(); + + if (vorbisCommentValid) { + std::vector vorbisComments = + context->parser->getVorbisComments(); + + jclass java_util_ArrayList = env->FindClass("java/util/ArrayList"); + jmethodID java_util_ArrayList_ = + env->GetMethodID(java_util_ArrayList, "", "(I)V"); + jmethodID java_util_ArrayList_add = + env->GetMethodID(java_util_ArrayList, "add", "(Ljava/lang/Object;)Z"); + commentArrayList = env->NewObject(java_util_ArrayList, java_util_ArrayList_, + vorbisComments.size()); + + for (std::vector::const_iterator comment = vorbisComments.begin(); + comment != vorbisComments.end(); ++comment) { + jstring element = env->NewStringUTF((*comment).c_str()); + env->CallBooleanMethod(commentArrayList, java_util_ArrayList_add, + element); + env->DeleteLocalRef(element); + } + } + const FLAC__StreamMetadata_StreamInfo &streamInfo = context->parser->getStreamInfo(); - jclass cls = env->FindClass( - "com/google/android/exoplayer2/util/" - "FlacStreamInfo"); - jmethodID constructor = env->GetMethodID(cls, "", "(IIIIIIIJ)V"); + jclass cls = env->FindClass("com/google/android/exoplayer2/util/" + "FlacStreamInfo"); + jmethodID constructor = env->GetMethodID(cls, "", + "(IIIIIIIJLjava/util/ArrayList;)V"); return env->NewObject(cls, constructor, streamInfo.min_blocksize, streamInfo.max_blocksize, streamInfo.min_framesize, streamInfo.max_framesize, streamInfo.sample_rate, streamInfo.channels, streamInfo.bits_per_sample, - streamInfo.total_samples); -} - -DECODER_FUNC(jobject, flacDecodeVorbisComment, jlong jContext) { - Context *context = reinterpret_cast(jContext); - context->source->setFlacDecoderJni(env, thiz); - - VorbisComment vorbisComment = context->parser->getVorbisComment(); - - if (vorbisComment.numComments == 0) { - return NULL; - } else { - jclass java_util_ArrayList = env->FindClass("java/util/ArrayList"); - - jmethodID java_util_ArrayList_ = env->GetMethodID(java_util_ArrayList, "", "(I)V"); - jmethodID java_util_ArrayList_add = env->GetMethodID(java_util_ArrayList, "add", - "(Ljava/lang/Object;)Z"); - - jobject result = env->NewObject(java_util_ArrayList, java_util_ArrayList_, - vorbisComment.numComments); - for (FLAC__uint32 i = 0; i < vorbisComment.numComments; ++i) { - jstring element = env->NewStringUTF(vorbisComment.metadataArray[i]); - env->CallBooleanMethod(result, java_util_ArrayList_add, element); - env->DeleteLocalRef(element); - } - return result; - } + streamInfo.total_samples, commentArrayList); } DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) { diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index 06c98302fd..9af7ec5c8a 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -174,24 +174,16 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) { break; case FLAC__METADATA_TYPE_VORBIS_COMMENT: if (!mVorbisCommentValid) { - FLAC__uint32 count = 0; - const FLAC__StreamMetadata_VorbisComment *vc = - &metadata->data.vorbis_comment; - mVorbisCommentValid = true; - mVorbisComment.metadataArray = - (char **) malloc(vc->num_comments * sizeof(char *)); - for (FLAC__uint32 i = 0; i < vc->num_comments; ++i) { - FLAC__StreamMetadata_VorbisComment_Entry *vce = &vc->comments[i]; - if (vce->entry != NULL) { - mVorbisComment.metadataArray[count] = - (char *) malloc((vce->length + 1) * sizeof(char)); - memcpy(mVorbisComment.metadataArray[count], vce->entry, - vce->length); - mVorbisComment.metadataArray[count][vce->length] = '\0'; - count++; + FLAC__StreamMetadata_VorbisComment vc = metadata->data.vorbis_comment; + for (FLAC__uint32 i = 0; i < vc.num_comments; ++i) { + FLAC__StreamMetadata_VorbisComment_Entry vce = vc.comments[i]; + if (vce.entry != NULL) { + std::string comment(reinterpret_cast(vce.entry), + vce.length); + mVorbisComments.push_back(comment); } } - mVorbisComment.numComments = count; + mVorbisCommentValid = true; } else { ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT"); } @@ -265,7 +257,6 @@ FLACParser::FLACParser(DataSource *source) ALOGV("FLACParser::FLACParser"); memset(&mStreamInfo, 0, sizeof(mStreamInfo)); memset(&mWriteHeader, 0, sizeof(mWriteHeader)); - memset(&mVorbisComment, 0, sizeof(mVorbisComment)); } FLACParser::~FLACParser() { diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index aec07d673e..8b70fea4cb 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -18,6 +18,9 @@ #define FLAC_PARSER_H_ #include +#include +#include +#include // libFLAC parser #include "FLAC/stream_decoder.h" @@ -26,11 +29,6 @@ typedef int status_t; -typedef struct VorbisComment_ { - int numComments; - char **metadataArray; -} VorbisComment; - class FLACParser { public: FLACParser(DataSource *source); @@ -49,6 +47,14 @@ class FLACParser { return mStreamInfo; } + bool isVorbisCommentValid() { + return mVorbisCommentValid; + } + + std::vector getVorbisComments() { + return mVorbisComments; + } + int64_t getLastFrameTimestamp() const { return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); } @@ -77,6 +83,7 @@ class FLACParser { if (newPosition == 0) { mStreamInfoValid = false; mVorbisCommentValid = false; + mVorbisComments.clear(); FLAC__stream_decoder_reset(mDecoder); } else { FLAC__stream_decoder_flush(mDecoder); @@ -102,10 +109,6 @@ class FLACParser { FLAC__STREAM_DECODER_END_OF_STREAM; } - VorbisComment getVorbisComment() { - return mVorbisComment; - } - private: DataSource *mDataSource; @@ -126,6 +129,8 @@ class FLACParser { const FLAC__StreamMetadata_SeekTable *mSeekTable; uint64_t firstFrameOffset; + // cached when the VORBIS_COMMENT metadata is parsed by libFLAC + std::vector mVorbisComments; bool mVorbisCommentValid; // cached when a decoded PCM block is "written" by libFLAC parser @@ -141,8 +146,6 @@ class FLACParser { FLACParser(const FLACParser &); FLACParser &operator=(const FLACParser &); - VorbisComment mVorbisComment; - // FLAC parser callbacks as C++ instance methods FLAC__StreamDecoderReadStatus readCallback(FLAC__byte buffer[], size_t *bytes); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java deleted file mode 100644 index a212532685..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2019 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.vorbis; - -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.metadata.Metadata; -import java.util.ArrayList; - -/** Decodes vorbis comments */ -public class VorbisCommentDecoder { - - private static final String SEPARATOR = "="; - - /** - * Decodes an {@link ArrayList} of vorbis comments. - * - * @param metadataStringList An {@link ArrayList} containing vorbis comments as {@link String} - * @return A {@link Metadata} structure with the vorbis comments as its entries. - */ - public Metadata decodeVorbisComments(@Nullable ArrayList metadataStringList) { - if (metadataStringList == null || metadataStringList.size() == 0) { - return null; - } - - ArrayList vorbisCommentFrames = new ArrayList<>(); - VorbisCommentFrame vorbisCommentFrame; - - for (String commentEntry : metadataStringList) { - String[] keyValue; - - keyValue = commentEntry.split(SEPARATOR); - if (keyValue.length != 2) { - /* Could not parse this comment, no key value pair found */ - continue; - } - vorbisCommentFrame = new VorbisCommentFrame(keyValue[0], keyValue[1]); - vorbisCommentFrames.add(vorbisCommentFrame); - } - - if (vorbisCommentFrames.size() > 0) { - return new Metadata(vorbisCommentFrames); - } else { - return null; - } - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java index 2deb5b1127..16bf333902 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java @@ -23,11 +23,12 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; /** Base class for Vorbis Comment Frames. */ -public class VorbisCommentFrame implements Metadata.Entry { +public final class VorbisCommentFrame implements Metadata.Entry { - /** The frame key and value */ + /** The key for this vorbis comment */ public final String key; + /** The value corresponding to this vorbis comment's key */ public final String value; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java index 0df39e103d..2d70402bdb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java @@ -15,7 +15,11 @@ */ package com.google.android.exoplayer2.util; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.vorbis.VorbisCommentFrame; +import java.util.ArrayList; /** * Holder for FLAC stream info. @@ -30,6 +34,10 @@ public final class FlacStreamInfo { public final int channels; public final int bitsPerSample; public final long totalSamples; + @Nullable + public final Metadata vorbisComments; + + private static final String SEPARATOR="="; /** * Constructs a FlacStreamInfo parsing the given binary FLAC stream info metadata structure. @@ -52,6 +60,7 @@ public final class FlacStreamInfo { this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL); // Remaining 16 bytes is md5 value + this.vorbisComments = null; } /** @@ -85,6 +94,78 @@ public final class FlacStreamInfo { this.channels = channels; this.bitsPerSample = bitsPerSample; this.totalSamples = totalSamples; + this.vorbisComments = null; + } + + /** + * Constructs a FlacStreamInfo given the parameters. + * + * @param minBlockSize Minimum block size of the FLAC stream. + * @param maxBlockSize Maximum block size of the FLAC stream. + * @param minFrameSize Minimum frame size of the FLAC stream. + * @param maxFrameSize Maximum frame size of the FLAC stream. + * @param sampleRate Sample rate of the FLAC stream. + * @param channels Number of channels of the FLAC stream. + * @param bitsPerSample Number of bits per sample of the FLAC stream. + * @param totalSamples Total samples of the FLAC stream. + * @param vorbisCommentList An {@link ArrayList} that contains vorbis comments, which will + * be converted and stored as metadata in {@link FlacStreamInfo#vorbisComments} + * @see FLAC format + * METADATA_BLOCK_STREAMINFO + */ + public FlacStreamInfo( + int minBlockSize, + int maxBlockSize, + int minFrameSize, + int maxFrameSize, + int sampleRate, + int channels, + int bitsPerSample, + long totalSamples, + ArrayList vorbisCommentList) { + this.minBlockSize = minBlockSize; + this.maxBlockSize = maxBlockSize; + this.minFrameSize = minFrameSize; + this.maxFrameSize = maxFrameSize; + this.sampleRate = sampleRate; + this.channels = channels; + this.bitsPerSample = bitsPerSample; + this.totalSamples = totalSamples; + this.vorbisComments = decodeVorbisComments(vorbisCommentList); + } + + /** + * Decodes an {@link ArrayList} of vorbis comments. + * + * @param metadataStringList An {@link ArrayList} containing vorbis comments as {@link String} + * @return A {@link Metadata} structure with the vorbis comments as its entries. + */ + @Nullable + private static Metadata decodeVorbisComments(@Nullable ArrayList metadataStringList) { + if (metadataStringList == null || metadataStringList.isEmpty()) { + return null; + } + + ArrayList vorbisCommentFrames = new ArrayList<>(); + VorbisCommentFrame vorbisCommentFrame; + + for (String commentEntry : metadataStringList) { + String[] keyValue; + + keyValue = commentEntry.split(SEPARATOR, 2); + if (keyValue.length != 2) { + /* Could not parse this comment, no key value pair found */ + continue; + } + vorbisCommentFrame = new VorbisCommentFrame(keyValue[0], keyValue[1]); + vorbisCommentFrames.add(vorbisCommentFrame); + } + + if (vorbisCommentFrames.isEmpty()) { + return null; + } else { + return new Metadata(vorbisCommentFrames); + } } /** Returns the maximum size for a decoded frame from the FLAC stream. */ diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java index e2c2bcf021..11b373327b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java @@ -19,72 +19,72 @@ import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.util.FlacStreamInfo; import java.util.ArrayList; import org.junit.Test; import org.junit.runner.RunWith; -/** Test for {@link VorbisCommentDecoder}. */ +/** Test for {@link FlacStreamInfo}'s conversion of {@link ArrayList} to {@link Metadata}. */ @RunWith(AndroidJUnit4.class) public final class VorbisCommentDecoderTest { @Test public void decode() { - VorbisCommentDecoder decoder = new VorbisCommentDecoder(); ArrayList commentsList = new ArrayList<>(); - commentsList.add("Title=Test"); - commentsList.add("Artist=Test2"); + commentsList.add("Title=Song"); + commentsList.add("Artist=Singer"); - Metadata metadata = decoder.decodeVorbisComments(commentsList); + Metadata metadata = new FlacStreamInfo(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; assertThat(metadata.length()).isEqualTo(2); VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); assertThat(commentFrame.key).isEqualTo("Title"); - assertThat(commentFrame.value).isEqualTo("Test"); + assertThat(commentFrame.value).isEqualTo("Song"); commentFrame = (VorbisCommentFrame) metadata.get(1); assertThat(commentFrame.key).isEqualTo("Artist"); - assertThat(commentFrame.value).isEqualTo("Test2"); + assertThat(commentFrame.value).isEqualTo("Singer"); } @Test public void decodeEmptyList() { - VorbisCommentDecoder decoder = new VorbisCommentDecoder(); ArrayList commentsList = new ArrayList<>(); - Metadata metadata = decoder.decodeVorbisComments(commentsList); + Metadata metadata = new FlacStreamInfo(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; assertThat(metadata).isNull(); } @Test public void decodeTwoSeparators() { - VorbisCommentDecoder decoder = new VorbisCommentDecoder(); ArrayList commentsList = new ArrayList<>(); - commentsList.add("Title=Test"); - commentsList.add("Artist=Test=2"); + commentsList.add("Title=Song"); + commentsList.add("Artist=Sing=er"); - Metadata metadata = decoder.decodeVorbisComments(commentsList); + Metadata metadata = new FlacStreamInfo(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; - assertThat(metadata.length()).isEqualTo(1); + assertThat(metadata.length()).isEqualTo(2); VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); assertThat(commentFrame.key).isEqualTo("Title"); - assertThat(commentFrame.value).isEqualTo("Test"); + assertThat(commentFrame.value).isEqualTo("Song"); + commentFrame = (VorbisCommentFrame) metadata.get(1); + assertThat(commentFrame.key).isEqualTo("Artist"); + assertThat(commentFrame.value).isEqualTo("Sing=er"); } @Test public void decodeNoSeparators() { - VorbisCommentDecoder decoder = new VorbisCommentDecoder(); ArrayList commentsList = new ArrayList<>(); - commentsList.add("TitleTest"); - commentsList.add("Artist=Test2"); + commentsList.add("TitleSong"); + commentsList.add("Artist=Singer"); - Metadata metadata = decoder.decodeVorbisComments(commentsList); + Metadata metadata = new FlacStreamInfo(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; assertThat(metadata.length()).isEqualTo(1); VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); assertThat(commentFrame.key).isEqualTo("Artist"); - assertThat(commentFrame.value).isEqualTo("Test2"); + assertThat(commentFrame.value).isEqualTo("Singer"); } } From 4b776ffe4264a93be7266c50fecc03f87db4d18b Mon Sep 17 00:00:00 2001 From: "Venkatarama NG. Avadhani" Date: Wed, 10 Jul 2019 20:41:18 +0530 Subject: [PATCH 1432/1556] Refactor FlacStreamInfo to FlacStreamMetadata --- extensions/flac/proguard-rules.txt | 2 +- .../ext/flac/FlacBinarySearchSeekerTest.java | 4 +- .../ext/flac/FlacBinarySearchSeeker.java | 22 +++++----- .../exoplayer2/ext/flac/FlacDecoder.java | 10 ++--- .../exoplayer2/ext/flac/FlacDecoderJni.java | 14 +++--- .../exoplayer2/ext/flac/FlacExtractor.java | 44 +++++++++---------- extensions/flac/src/main/jni/flac_jni.cc | 2 +- .../exoplayer2/extractor/ogg/FlacReader.java | 28 ++++++++---- ...treamInfo.java => FlacStreamMetadata.java} | 16 +++---- .../vorbis/VorbisCommentDecoderTest.java | 12 ++--- 10 files changed, 82 insertions(+), 72 deletions(-) rename library/core/src/main/java/com/google/android/exoplayer2/util/{FlacStreamInfo.java => FlacStreamMetadata.java} (94%) diff --git a/extensions/flac/proguard-rules.txt b/extensions/flac/proguard-rules.txt index ee0a9fa5b5..b44dab3445 100644 --- a/extensions/flac/proguard-rules.txt +++ b/extensions/flac/proguard-rules.txt @@ -9,6 +9,6 @@ -keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni { *; } --keep class com.google.android.exoplayer2.util.FlacStreamInfo { +-keep class com.google.android.exoplayer2.util.FlacStreamMetadata { *; } diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java index 934d7cf106..b469a92cb4 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java @@ -52,7 +52,7 @@ public final class FlacBinarySearchSeekerTest { FlacBinarySearchSeeker seeker = new FlacBinarySearchSeeker( - decoderJni.decodeStreamInfo(), /* firstFramePosition= */ 0, data.length, decoderJni); + decoderJni.decodeStreamMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); SeekMap seekMap = seeker.getSeekMap(); assertThat(seekMap).isNotNull(); @@ -70,7 +70,7 @@ public final class FlacBinarySearchSeekerTest { decoderJni.setData(input); FlacBinarySearchSeeker seeker = new FlacBinarySearchSeeker( - decoderJni.decodeStreamInfo(), /* firstFramePosition= */ 0, data.length, decoderJni); + decoderJni.decodeStreamMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); seeker.setSeekTargetUs(/* timeUs= */ 1000); assertThat(seeker.isSeeking()).isTrue(); diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java index b9c6ea06dd..4bfcc003ec 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java @@ -19,7 +19,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import java.io.IOException; import java.nio.ByteBuffer; @@ -34,20 +34,20 @@ import java.nio.ByteBuffer; private final FlacDecoderJni decoderJni; public FlacBinarySearchSeeker( - FlacStreamInfo streamInfo, + FlacStreamMetadata streamMetadata, long firstFramePosition, long inputLength, FlacDecoderJni decoderJni) { super( - new FlacSeekTimestampConverter(streamInfo), + new FlacSeekTimestampConverter(streamMetadata), new FlacTimestampSeeker(decoderJni), - streamInfo.durationUs(), + streamMetadata.durationUs(), /* floorTimePosition= */ 0, - /* ceilingTimePosition= */ streamInfo.totalSamples, + /* ceilingTimePosition= */ streamMetadata.totalSamples, /* floorBytePosition= */ firstFramePosition, /* ceilingBytePosition= */ inputLength, - /* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(), - /* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize)); + /* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(), + /* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize)); this.decoderJni = Assertions.checkNotNull(decoderJni); } @@ -112,15 +112,15 @@ import java.nio.ByteBuffer; * the timestamp for a stream seek time position. */ private static final class FlacSeekTimestampConverter implements SeekTimestampConverter { - private final FlacStreamInfo streamInfo; + private final FlacStreamMetadata streamMetadata; - public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) { - this.streamInfo = streamInfo; + public FlacSeekTimestampConverter(FlacStreamMetadata streamMetadata) { + this.streamMetadata = streamMetadata; } @Override public long timeUsToTargetTime(long timeUs) { - return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs); + return Assertions.checkNotNull(streamMetadata).getSampleIndex(timeUs); } } } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java index d20c18e957..50eb048d98 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java @@ -21,7 +21,7 @@ import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; -import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import java.io.IOException; import java.nio.ByteBuffer; import java.util.List; @@ -58,9 +58,9 @@ import java.util.List; } decoderJni = new FlacDecoderJni(); decoderJni.setData(ByteBuffer.wrap(initializationData.get(0))); - FlacStreamInfo streamInfo; + FlacStreamMetadata streamMetadata; try { - streamInfo = decoderJni.decodeStreamInfo(); + streamMetadata = decoderJni.decodeStreamMetadata(); } catch (ParserException e) { throw new FlacDecoderException("Failed to decode StreamInfo", e); } catch (IOException | InterruptedException e) { @@ -69,9 +69,9 @@ import java.util.List; } int initialInputBufferSize = - maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize; + maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize; setInitialInputBufferSize(initialInputBufferSize); - maxOutputBufferSize = streamInfo.maxDecodedFrameSize(); + maxOutputBufferSize = streamMetadata.maxDecodedFrameSize(); } @Override diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java index 32ef22dab0..bf9dff9e52 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java @@ -19,7 +19,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.nio.ByteBuffer; @@ -142,13 +142,13 @@ import java.nio.ByteBuffer; return byteCount; } - /** Decodes and consumes the StreamInfo section from the FLAC stream. */ - public FlacStreamInfo decodeStreamInfo() throws IOException, InterruptedException { - FlacStreamInfo streamInfo = flacDecodeMetadata(nativeDecoderContext); - if (streamInfo == null) { + /** Decodes and consumes the metadata from the FLAC stream. */ + public FlacStreamMetadata decodeStreamMetadata() throws IOException, InterruptedException { + FlacStreamMetadata streamMetadata = flacDecodeMetadata(nativeDecoderContext); + if (streamMetadata == null) { throw new ParserException("Failed to decode StreamInfo"); } - return streamInfo; + return streamMetadata; } /** @@ -266,7 +266,7 @@ import java.nio.ByteBuffer; private native long flacInit(); - private native FlacStreamInfo flacDecodeMetadata(long context) + private native FlacStreamMetadata flacDecodeMetadata(long context) throws IOException, InterruptedException; private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 50e0458fd7..5061cb614f 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -34,7 +34,7 @@ import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; @@ -87,7 +87,7 @@ public final class FlacExtractor implements Extractor { private @MonotonicNonNull TrackOutput trackOutput; private boolean streamInfoDecoded; - private @MonotonicNonNull FlacStreamInfo streamInfo; + private @MonotonicNonNull FlacStreamMetadata streamMetadata; private @MonotonicNonNull OutputFrameHolder outputFrameHolder; @Nullable private Metadata id3Metadata; @@ -207,16 +207,16 @@ public final class FlacExtractor implements Extractor { } @RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized. - @EnsuresNonNull({"streamInfo", "outputFrameHolder"}) // Ensures StreamInfo decoded. + @EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures StreamInfo decoded. @SuppressWarnings({"contracts.postcondition.not.satisfied"}) private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, IOException { if (streamInfoDecoded) { return; } - FlacStreamInfo streamInfo; + FlacStreamMetadata streamMetadata; try { - streamInfo = decoderJni.decodeStreamInfo(); + streamMetadata = decoderJni.decodeStreamMetadata(); } catch (IOException e) { decoderJni.reset(/* newPosition= */ 0); input.setRetryPosition(/* position= */ 0, e); @@ -224,16 +224,16 @@ public final class FlacExtractor implements Extractor { } streamInfoDecoded = true; - if (this.streamInfo == null) { - this.streamInfo = streamInfo; + if (this.streamMetadata == null) { + this.streamMetadata = streamMetadata; binarySearchSeeker = - outputSeekMap(decoderJni, streamInfo, input.getLength(), extractorOutput); + outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput); Metadata metadata = id3MetadataDisabled ? null : id3Metadata; - if (streamInfo.vorbisComments != null) { - metadata = streamInfo.vorbisComments.copyWithAppendedEntriesFrom(metadata); + if (streamMetadata.vorbisComments != null) { + metadata = streamMetadata.vorbisComments.copyWithAppendedEntriesFrom(metadata); } - outputFormat(streamInfo, metadata, trackOutput); - outputBuffer.reset(streamInfo.maxDecodedFrameSize()); + outputFormat(streamMetadata, metadata, trackOutput); + outputBuffer.reset(streamMetadata.maxDecodedFrameSize()); outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); } } @@ -273,38 +273,38 @@ public final class FlacExtractor implements Extractor { @Nullable private static FlacBinarySearchSeeker outputSeekMap( FlacDecoderJni decoderJni, - FlacStreamInfo streamInfo, + FlacStreamMetadata streamMetadata, long streamLength, ExtractorOutput output) { boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1; FlacBinarySearchSeeker binarySearchSeeker = null; SeekMap seekMap; if (hasSeekTable) { - seekMap = new FlacSeekMap(streamInfo.durationUs(), decoderJni); + seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni); } else if (streamLength != C.LENGTH_UNSET) { long firstFramePosition = decoderJni.getDecodePosition(); binarySearchSeeker = - new FlacBinarySearchSeeker(streamInfo, firstFramePosition, streamLength, decoderJni); + new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni); seekMap = binarySearchSeeker.getSeekMap(); } else { - seekMap = new SeekMap.Unseekable(streamInfo.durationUs()); + seekMap = new SeekMap.Unseekable(streamMetadata.durationUs()); } output.seekMap(seekMap); return binarySearchSeeker; } private static void outputFormat( - FlacStreamInfo streamInfo, @Nullable Metadata metadata, TrackOutput output) { + FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) { Format mediaFormat = Format.createAudioSampleFormat( /* id= */ null, MimeTypes.AUDIO_RAW, /* codecs= */ null, - streamInfo.bitRate(), - streamInfo.maxDecodedFrameSize(), - streamInfo.channels, - streamInfo.sampleRate, - getPcmEncoding(streamInfo.bitsPerSample), + streamMetadata.bitRate(), + streamMetadata.maxDecodedFrameSize(), + streamMetadata.channels, + streamMetadata.sampleRate, + getPcmEncoding(streamMetadata.bitsPerSample), /* encoderDelay= */ 0, /* encoderPadding= */ 0, /* initializationData= */ null, diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 600f181890..22b581489f 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -124,7 +124,7 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { context->parser->getStreamInfo(); jclass cls = env->FindClass("com/google/android/exoplayer2/util/" - "FlacStreamInfo"); + "FlacStreamMetadata"); jmethodID constructor = env->GetMethodID(cls, "", "(IIIIIIIJLjava/util/ArrayList;)V"); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java index 5eb0727908..d4c2bbb485 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java @@ -19,7 +19,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekPoint; -import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -38,7 +38,7 @@ import java.util.List; private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4; - private FlacStreamInfo streamInfo; + private FlacStreamMetadata streamMetadata; private FlacOggSeeker flacOggSeeker; public static boolean verifyBitstreamType(ParsableByteArray data) { @@ -50,7 +50,7 @@ import java.util.List; protected void reset(boolean headerData) { super.reset(headerData); if (headerData) { - streamInfo = null; + streamMetadata = null; flacOggSeeker = null; } } @@ -71,14 +71,24 @@ import java.util.List; protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) throws IOException, InterruptedException { byte[] data = packet.data; - if (streamInfo == null) { - streamInfo = new FlacStreamInfo(data, 17); + if (streamMetadata == null) { + streamMetadata = new FlacStreamMetadata(data, 17); byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit()); metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks List initializationData = Collections.singletonList(metadata); - setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC, null, - Format.NO_VALUE, streamInfo.bitRate(), streamInfo.channels, streamInfo.sampleRate, - initializationData, null, 0, null); + setupData.format = + Format.createAudioSampleFormat( + null, + MimeTypes.AUDIO_FLAC, + null, + Format.NO_VALUE, + streamMetadata.bitRate(), + streamMetadata.channels, + streamMetadata.sampleRate, + initializationData, + null, + 0, + null); } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) { flacOggSeeker = new FlacOggSeeker(); flacOggSeeker.parseSeekTable(packet); @@ -211,7 +221,7 @@ import java.util.List; @Override public long getDurationUs() { - return streamInfo.durationUs(); + return streamMetadata.durationUs(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java similarity index 94% rename from library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java index 2d70402bdb..abccbd3c03 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java @@ -24,7 +24,7 @@ import java.util.ArrayList; /** * Holder for FLAC stream info. */ -public final class FlacStreamInfo { +public final class FlacStreamMetadata { public final int minBlockSize; public final int maxBlockSize; @@ -40,14 +40,14 @@ public final class FlacStreamInfo { private static final String SEPARATOR="="; /** - * Constructs a FlacStreamInfo parsing the given binary FLAC stream info metadata structure. + * Constructs a FlacStreamMetadata parsing the given binary FLAC stream info metadata structure. * * @param data An array holding FLAC stream info metadata structure * @param offset Offset of the structure in the array * @see FLAC format * METADATA_BLOCK_STREAMINFO */ - public FlacStreamInfo(byte[] data, int offset) { + public FlacStreamMetadata(byte[] data, int offset) { ParsableBitArray scratch = new ParsableBitArray(data); scratch.setPosition(offset * 8); this.minBlockSize = scratch.readBits(16); @@ -64,7 +64,7 @@ public final class FlacStreamInfo { } /** - * Constructs a FlacStreamInfo given the parameters. + * Constructs a FlacStreamMetadata given the parameters. * * @param minBlockSize Minimum block size of the FLAC stream. * @param maxBlockSize Maximum block size of the FLAC stream. @@ -77,7 +77,7 @@ public final class FlacStreamInfo { * @see FLAC format * METADATA_BLOCK_STREAMINFO */ - public FlacStreamInfo( + public FlacStreamMetadata( int minBlockSize, int maxBlockSize, int minFrameSize, @@ -98,7 +98,7 @@ public final class FlacStreamInfo { } /** - * Constructs a FlacStreamInfo given the parameters. + * Constructs a FlacStreamMetadata given the parameters. * * @param minBlockSize Minimum block size of the FLAC stream. * @param maxBlockSize Maximum block size of the FLAC stream. @@ -109,11 +109,11 @@ public final class FlacStreamInfo { * @param bitsPerSample Number of bits per sample of the FLAC stream. * @param totalSamples Total samples of the FLAC stream. * @param vorbisCommentList An {@link ArrayList} that contains vorbis comments, which will - * be converted and stored as metadata in {@link FlacStreamInfo#vorbisComments} + * be converted and stored as metadata in {@link FlacStreamMetadata#vorbisComments} * @see FLAC format * METADATA_BLOCK_STREAMINFO */ - public FlacStreamInfo( + public FlacStreamMetadata( int minBlockSize, int maxBlockSize, int minFrameSize, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java index 11b373327b..504dce62b9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java @@ -19,12 +19,12 @@ import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import java.util.ArrayList; import org.junit.Test; import org.junit.runner.RunWith; -/** Test for {@link FlacStreamInfo}'s conversion of {@link ArrayList} to {@link Metadata}. */ +/** Test for {@link FlacStreamMetadata}'s conversion of {@link ArrayList} to {@link Metadata}. */ @RunWith(AndroidJUnit4.class) public final class VorbisCommentDecoderTest { @@ -35,7 +35,7 @@ public final class VorbisCommentDecoderTest { commentsList.add("Title=Song"); commentsList.add("Artist=Singer"); - Metadata metadata = new FlacStreamInfo(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; assertThat(metadata.length()).isEqualTo(2); VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); @@ -50,7 +50,7 @@ public final class VorbisCommentDecoderTest { public void decodeEmptyList() { ArrayList commentsList = new ArrayList<>(); - Metadata metadata = new FlacStreamInfo(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; assertThat(metadata).isNull(); } @@ -62,7 +62,7 @@ public final class VorbisCommentDecoderTest { commentsList.add("Title=Song"); commentsList.add("Artist=Sing=er"); - Metadata metadata = new FlacStreamInfo(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; assertThat(metadata.length()).isEqualTo(2); VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); @@ -80,7 +80,7 @@ public final class VorbisCommentDecoderTest { commentsList.add("TitleSong"); commentsList.add("Artist=Singer"); - Metadata metadata = new FlacStreamInfo(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; assertThat(metadata.length()).isEqualTo(1); VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); From 29a099cf03236edc6d46a262d64621dfcdac8989 Mon Sep 17 00:00:00 2001 From: Yannick RUI Date: Fri, 12 Jul 2019 13:28:41 +0200 Subject: [PATCH 1433/1556] Switch text track score from the score based logic to a comparison based logic similar to the one we use for audio track selection (see AudioTrackScore). --- .../trackselection/DefaultTrackSelector.java | 154 ++++++++++-------- 1 file changed, 84 insertions(+), 70 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 949bd178ea..511a974a0e 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 @@ -1552,38 +1552,30 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } - int selectedTextTrackScore = Integer.MIN_VALUE; + TextTrackScore selectedTextTrackScore = null; int selectedTextRendererIndex = C.INDEX_UNSET; for (int i = 0; i < rendererCount; i++) { - int trackType = mappedTrackInfo.getRendererType(i); - switch (trackType) { - case C.TRACK_TYPE_VIDEO: - case C.TRACK_TYPE_AUDIO: - // Already done. Do nothing. - break; - case C.TRACK_TYPE_TEXT: - Pair textSelection = - selectTextTrack( - mappedTrackInfo.getTrackGroups(i), - rendererFormatSupports[i], - params, - selectedAudioLanguage); - if (textSelection != null && textSelection.second > selectedTextTrackScore) { - if (selectedTextRendererIndex != C.INDEX_UNSET) { - // We've already made a selection for another text renderer, but it had a lower score. - // Clear the selection for that renderer. - definitions[selectedTextRendererIndex] = null; - } - definitions[i] = textSelection.first; - selectedTextTrackScore = textSelection.second; - selectedTextRendererIndex = i; + // The below behaviour is different from video and audio track selection + // i.e. do not perform a text track pre selection if there are no preferredTextLanguage requested. + if (C.TRACK_TYPE_TEXT == mappedTrackInfo.getRendererType(i) && params.preferredTextLanguage != null) { + Pair textSelection = + selectTextTrack( + mappedTrackInfo.getTrackGroups(i), + rendererFormatSupports[i], + params); + if (textSelection != null + && (selectedTextTrackScore == null + || textSelection.second.compareTo(selectedTextTrackScore) > 0)) { + if (selectedTextRendererIndex != C.INDEX_UNSET) { + // We've already made a selection for another text renderer, but it had a lower + // score. Clear the selection for that renderer. + definitions[selectedTextRendererIndex] = null; } - break; - default: - definitions[i] = - selectOtherTrack( - trackType, mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params); - break; + TrackSelection.Definition definition = textSelection.first; + definitions[i] = definition; + selectedTextTrackScore = textSelection.second; + selectedTextRendererIndex = i; + } } } @@ -2051,22 +2043,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped * track, indexed by track group index and track index (in that order). * @param params The selector's current constraint parameters. - * @param selectedAudioLanguage The language of the selected audio track. May be null if the - * selected audio track declares no language or no audio track was selected. - * @return The {@link TrackSelection.Definition} and corresponding track score, or null if no + * selected text track declares no language or no text track was selected. + * @return The {@link TrackSelection.Definition} and corresponding {@link TextTrackScore}, or null if no * selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ @Nullable - protected Pair selectTextTrack( + protected Pair selectTextTrack( TrackGroupArray groups, int[][] formatSupport, - Parameters params, - @Nullable String selectedAudioLanguage) + Parameters params) throws ExoPlaybackException { TrackGroup selectedGroup = null; - int selectedTrackIndex = 0; - int selectedTrackScore = 0; + int selectedTrackIndex = C.INDEX_UNSET; + TextTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); int[] trackFormatSupport = formatSupport[groupIndex]; @@ -2074,39 +2064,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); - int maskedSelectionFlags = - format.selectionFlags & ~params.disabledTextTrackSelectionFlags; - boolean isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; - boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; - int trackScore; - int languageScore = getFormatLanguageScore(format, params.preferredTextLanguage); - boolean trackHasNoLanguage = formatHasNoLanguage(format); - if (languageScore > 0 || (params.selectUndeterminedTextLanguage && trackHasNoLanguage)) { - if (isDefault) { - trackScore = 11; - } else if (!isForced) { - // Prefer non-forced to forced if a preferred text language has been specified. Where - // both are provided the non-forced track will usually contain the forced subtitles as - // a subset. - trackScore = 7; - } else { - trackScore = 3; - } - trackScore += languageScore; - } else if (isDefault) { - trackScore = 2; - } else if (isForced - && (getFormatLanguageScore(format, selectedAudioLanguage) > 0 - || (trackHasNoLanguage && stringDefinesNoLanguage(selectedAudioLanguage)))) { - trackScore = 1; - } else { - // Track should not be selected. - continue; - } - if (isSupported(trackFormatSupport[trackIndex], false)) { - trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; - } - if (trackScore > selectedTrackScore) { + TextTrackScore trackScore = new TextTrackScore(format, params, trackFormatSupport[trackIndex]); + if ((selectedTrackScore == null) || trackScore.compareTo(selectedTrackScore) > 0) { selectedGroup = trackGroup; selectedTrackIndex = trackIndex; selectedTrackScore = trackScore; @@ -2535,4 +2494,59 @@ public class DefaultTrackSelector extends MappingTrackSelector { } + /** Represents how well an text track matches the selection {@link Parameters}. */ + protected static final class TextTrackScore implements Comparable { + + private final boolean isWithinRendererCapabilities; + private final int preferredLanguageScore; + private final int localeLanguageMatchIndex; + private final int localeLanguageScore; + private final boolean isDefaultSelectionFlag; + + public TextTrackScore(Format format, Parameters parameters, int formatSupport) { + isWithinRendererCapabilities = isSupported(formatSupport, false); + preferredLanguageScore = getFormatLanguageScore(format, parameters.preferredTextLanguage); + isDefaultSelectionFlag = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; + String[] localeLanguages = Util.getSystemLanguageCodes(); + int bestMatchIndex = Integer.MAX_VALUE; + int bestMatchScore = 0; + for (int i = 0; i < localeLanguages.length; i++) { + int score = getFormatLanguageScore(format, localeLanguages[i]); + if (score > 0) { + bestMatchIndex = i; + bestMatchScore = score; + break; + } + } + localeLanguageMatchIndex = bestMatchIndex; + localeLanguageScore = bestMatchScore; + } + + /** + * Compares this score with another. + * + * @param other The other score to compare to. + * @return A positive integer if this score is better than the other. Zero if they are equal. A + * negative integer if this score is worse than the other. + */ + @Override + public int compareTo(@NonNull TextTrackScore other) { + if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { + return this.isWithinRendererCapabilities ? 1 : -1; + } + if (this.preferredLanguageScore != other.preferredLanguageScore) { + return compareInts(this.preferredLanguageScore, other.preferredLanguageScore); + } + if (this.isDefaultSelectionFlag != other.isDefaultSelectionFlag) { + return this.isDefaultSelectionFlag ? 1 : -1; + } + if (this.localeLanguageMatchIndex != other.localeLanguageMatchIndex) { + return -compareInts(this.localeLanguageMatchIndex, other.localeLanguageMatchIndex); + } + if (this.localeLanguageScore != other.localeLanguageScore) { + return compareInts(this.localeLanguageScore, other.localeLanguageScore); + } + return 0; + } + } } From 49a2e5a5cba54c6f0099d5ba7df91059cc99a833 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 10 Jul 2019 09:52:53 +0100 Subject: [PATCH 1434/1556] add manifest to Timeline.Window - Remove manifest argument from callbacks of Player.EventListener and SourceInfoRefreshListener. Instead make it accessible through Player.getCurrentManifest() and Timeline.Window.manifest. - Fix all MediaSource implementation to include the manifest in the Timeline instead of passing it to the SourceInfoRefreshListener. - Refactor ExoPlayerTestRunner, FakeTimeline, FakeMediaSource to reflect these changes and make tests pass. PiperOrigin-RevId: 257359662 --- RELEASENOTES.md | 3 + .../exoplayer2/castdemo/PlayerManager.java | 3 +- .../exoplayer2/ext/cast/CastPlayer.java | 8 +- .../exoplayer2/ext/cast/CastTimeline.java | 1 + .../exoplayer2/ext/ima/ImaAdsLoader.java | 3 +- .../exoplayer2/ext/ima/FakePlayer.java | 4 +- .../ext/leanback/LeanbackPlayerAdapter.java | 3 +- .../mediasession/MediaSessionConnector.java | 3 +- .../google/android/exoplayer2/BasePlayer.java | 14 +- .../android/exoplayer2/ExoPlayerImpl.java | 18 +-- .../exoplayer2/ExoPlayerImplInternal.java | 15 +- .../android/exoplayer2/PlaybackInfo.java | 20 +-- .../com/google/android/exoplayer2/Player.java | 45 +++++- .../android/exoplayer2/SimpleExoPlayer.java | 7 - .../google/android/exoplayer2/Timeline.java | 5 + .../analytics/AnalyticsCollector.java | 3 +- .../exoplayer2/offline/DownloadHelper.java | 11 +- .../exoplayer2/source/BaseMediaSource.java | 14 +- .../source/ClippingMediaSource.java | 7 +- .../source/CompositeMediaSource.java | 15 +- .../source/ConcatenatingMediaSource.java | 8 +- .../source/ExtractorMediaSource.java | 5 +- .../exoplayer2/source/LoopingMediaSource.java | 5 +- .../exoplayer2/source/MaskingMediaSource.java | 5 +- .../exoplayer2/source/MediaPeriod.java | 4 +- .../exoplayer2/source/MediaSource.java | 12 +- .../exoplayer2/source/MergingMediaSource.java | 9 +- .../source/ProgressiveMediaSource.java | 7 +- .../exoplayer2/source/SilenceMediaSource.java | 3 +- .../source/SinglePeriodTimeline.java | 20 ++- .../source/SingleSampleMediaSource.java | 5 +- .../exoplayer2/source/ads/AdsMediaSource.java | 14 +- .../android/exoplayer2/ExoPlayerTest.java | 135 ++++++++---------- .../exoplayer2/MediaPeriodQueueTest.java | 1 - .../analytics/AnalyticsCollectorTest.java | 37 ++--- .../offline/DownloadHelperTest.java | 8 +- .../source/ClippingMediaSourceTest.java | 17 ++- .../source/ConcatenatingMediaSourceTest.java | 33 +++-- .../source/LoopingMediaSourceTest.java | 4 +- .../source/MergingMediaSourceTest.java | 5 +- .../source/SinglePeriodTimelineTest.java | 9 +- .../source/dash/DashMediaSource.java | 3 +- .../exoplayer2/source/hls/HlsMediaSource.java | 5 +- .../source/smoothstreaming/SsMediaSource.java | 5 +- .../exoplayer2/ui/PlayerControlView.java | 3 +- .../ui/PlayerNotificationManager.java | 2 +- .../android/exoplayer2/testutil/Action.java | 8 +- .../testutil/ExoPlayerTestRunner.java | 30 +--- .../testutil/FakeAdaptiveMediaSource.java | 3 +- .../exoplayer2/testutil/FakeMediaSource.java | 13 +- .../exoplayer2/testutil/FakeTimeline.java | 19 ++- .../testutil/MediaSourceTestRunner.java | 2 +- .../exoplayer2/testutil/StubExoPlayer.java | 5 - 53 files changed, 316 insertions(+), 330 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e40bfe8d81..59f30b8a0a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,9 @@ * Add VR player demo. * Wrap decoder exceptions in a new `DecoderException` class and report as renderer error. +* Do not pass the manifest to callbacks of Player.EventListener and + SourceInfoRefreshListener anymore. Instead make it accessible through + Player.getCurrentManifest() and Timeline.Window.manifest. ### 2.10.3 ### 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 c92ebd7e94..d2a1ca0860 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 @@ -264,8 +264,7 @@ import org.json.JSONObject; } @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { + public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) { updateCurrentItemIndex(); } diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 03518ac18a..6a33aa0428 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -460,11 +460,6 @@ public final class CastPlayer extends BasePlayer { return currentTimeline; } - @Override - @Nullable public Object getCurrentManifest() { - return null; - } - @Override public int getCurrentPeriodIndex() { return getCurrentWindowIndex(); @@ -592,8 +587,7 @@ public final class CastPlayer extends BasePlayer { waitingForInitialTimeline = false; notificationsBatch.add( new ListenerNotificationTask( - listener -> - listener.onTimelineChanged(currentTimeline, /* manifest= */ null, reason))); + listener -> listener.onTimelineChanged(currentTimeline, reason))); } } diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java index 800c19047b..b84f1c1f2b 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java @@ -117,6 +117,7 @@ import java.util.Arrays; Object tag = setTag ? ids[windowIndex] : null; return window.set( tag, + /* manifest= */ null, /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, /* isSeekable= */ !isDynamic, 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 5a266c290d..249271dc61 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 @@ -946,8 +946,7 @@ public final class ImaAdsLoader // Player.EventListener implementation. @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { + public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { if (timeline.isEmpty()) { // The player is being reset or contains no media. return; diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java index a9d6a37fac..a9572b7a8d 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java @@ -51,9 +51,7 @@ import java.util.ArrayList; public void updateTimeline(Timeline timeline) { for (Player.EventListener listener : listeners) { listener.onTimelineChanged( - timeline, - null, - prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED); + timeline, prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED); } prepared = true; } 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 1fece6bc8e..370e5515e8 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 @@ -288,8 +288,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab } @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { + public void onTimelineChanged(Timeline timeline, @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 3136e3cca9..be085ae30b 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 @@ -1020,8 +1020,7 @@ public final class MediaSessionConnector { // Player.EventListener implementation. @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { + public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { Player player = Assertions.checkNotNull(MediaSessionConnector.this.player); int windowCount = player.getCurrentTimeline().getWindowCount(); int windowIndex = player.getCurrentWindowIndex(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java index 774f1b452c..bb14ac147b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java @@ -94,11 +94,19 @@ public abstract class BasePlayer implements Player { @Override @Nullable public final Object getCurrentTag() { - int windowIndex = getCurrentWindowIndex(); Timeline timeline = getCurrentTimeline(); - return windowIndex >= timeline.getWindowCount() + return timeline.isEmpty() ? null - : timeline.getWindow(windowIndex, window, /* setTag= */ true).tag; + : timeline.getWindow(getCurrentWindowIndex(), window, /* setTag= */ true).tag; + } + + @Override + @Nullable + public final Object getCurrentManifest() { + Timeline timeline = getCurrentTimeline(); + return timeline.isEmpty() + ? null + : timeline.getWindow(getCurrentWindowIndex(), window, /* setTag= */ false).manifest; } @Override 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 945bd32d30..73107aa98e 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 @@ -547,11 +547,6 @@ import java.util.concurrent.CopyOnWriteArrayList; return playbackInfo.timeline; } - @Override - public Object getCurrentManifest() { - return playbackInfo.manifest; - } - // Not private so it can be called from an inner class without going through a thunk method. /* package */ void handleEvent(Message msg) { switch (msg.what) { @@ -639,7 +634,6 @@ import java.util.concurrent.CopyOnWriteArrayList; long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs; return new PlaybackInfo( resetState ? Timeline.EMPTY : playbackInfo.timeline, - resetState ? null : playbackInfo.manifest, mediaPeriodId, startPositionUs, contentPositionUs, @@ -713,7 +707,7 @@ import java.util.concurrent.CopyOnWriteArrayList; private final @Player.TimelineChangeReason int timelineChangeReason; private final boolean seekProcessed; private final boolean playbackStateChanged; - private final boolean timelineOrManifestChanged; + private final boolean timelineChanged; private final boolean isLoadingChanged; private final boolean trackSelectorResultChanged; private final boolean playWhenReady; @@ -737,9 +731,7 @@ import java.util.concurrent.CopyOnWriteArrayList; this.seekProcessed = seekProcessed; this.playWhenReady = playWhenReady; playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState; - timelineOrManifestChanged = - previousPlaybackInfo.timeline != playbackInfo.timeline - || previousPlaybackInfo.manifest != playbackInfo.manifest; + timelineChanged = previousPlaybackInfo.timeline != playbackInfo.timeline; isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading; trackSelectorResultChanged = previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult; @@ -747,12 +739,10 @@ import java.util.concurrent.CopyOnWriteArrayList; @Override public void run() { - if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { + if (timelineChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { invokeAll( listenerSnapshot, - listener -> - listener.onTimelineChanged( - playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason)); + listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason)); } if (positionDiscontinuity) { invokeAll( 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 a6d4352880..5f53427fca 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 @@ -267,9 +267,10 @@ import java.util.concurrent.atomic.AtomicBoolean; // MediaSource.SourceInfoRefreshListener implementation. @Override - public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) { - handler.obtainMessage(MSG_REFRESH_SOURCE_INFO, - new MediaSourceRefreshInfo(source, timeline, manifest)).sendToTarget(); + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { + handler + .obtainMessage(MSG_REFRESH_SOURCE_INFO, new MediaSourceRefreshInfo(source, timeline)) + .sendToTarget(); } // MediaPeriod.Callback implementation. @@ -899,7 +900,6 @@ import java.util.concurrent.atomic.AtomicBoolean; playbackInfo = new PlaybackInfo( resetState ? Timeline.EMPTY : playbackInfo.timeline, - resetState ? null : playbackInfo.manifest, mediaPeriodId, startPositionUs, contentPositionUs, @@ -1276,9 +1276,8 @@ import java.util.concurrent.atomic.AtomicBoolean; Timeline oldTimeline = playbackInfo.timeline; Timeline timeline = sourceRefreshInfo.timeline; - Object manifest = sourceRefreshInfo.manifest; queue.setTimeline(timeline); - playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest); + playbackInfo = playbackInfo.copyWithTimeline(timeline); resolvePendingMessagePositions(); MediaPeriodId newPeriodId = playbackInfo.periodId; @@ -1881,12 +1880,10 @@ import java.util.concurrent.atomic.AtomicBoolean; public final MediaSource source; public final Timeline timeline; - public final Object manifest; - public MediaSourceRefreshInfo(MediaSource source, Timeline timeline, Object manifest) { + public MediaSourceRefreshInfo(MediaSource source, Timeline timeline) { this.source = source; this.timeline = timeline; - this.manifest = manifest; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index d3e4a0e626..1eedae08b6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2; import androidx.annotation.CheckResult; -import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; @@ -35,8 +34,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /** The current {@link Timeline}. */ public final Timeline timeline; - /** The current manifest. */ - @Nullable public final Object manifest; /** The {@link MediaPeriodId} of the currently playing media period in the {@link #timeline}. */ public final MediaPeriodId periodId; /** @@ -91,7 +88,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; long startPositionUs, TrackSelectorResult emptyTrackSelectorResult) { return new PlaybackInfo( Timeline.EMPTY, - /* manifest= */ null, DUMMY_MEDIA_PERIOD_ID, startPositionUs, /* contentPositionUs= */ C.TIME_UNSET, @@ -109,7 +105,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; * Create playback info. * * @param timeline See {@link #timeline}. - * @param manifest See {@link #manifest}. * @param periodId See {@link #periodId}. * @param startPositionUs See {@link #startPositionUs}. * @param contentPositionUs See {@link #contentPositionUs}. @@ -124,7 +119,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; */ public PlaybackInfo( Timeline timeline, - @Nullable Object manifest, MediaPeriodId periodId, long startPositionUs, long contentPositionUs, @@ -137,7 +131,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; long totalBufferedDurationUs, long positionUs) { this.timeline = timeline; - this.manifest = manifest; this.periodId = periodId; this.startPositionUs = startPositionUs; this.contentPositionUs = contentPositionUs; @@ -187,7 +180,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; long totalBufferedDurationUs) { return new PlaybackInfo( timeline, - manifest, periodId, positionUs, periodId.isAd() ? contentPositionUs : C.TIME_UNSET, @@ -202,17 +194,15 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; } /** - * Copies playback info with new timeline and manifest. + * Copies playback info with the new timeline. * * @param timeline New timeline. See {@link #timeline}. - * @param manifest New manifest. See {@link #manifest}. - * @return Copied playback info with new timeline and manifest. + * @return Copied playback info with the new timeline. */ @CheckResult - public PlaybackInfo copyWithTimeline(Timeline timeline, Object manifest) { + public PlaybackInfo copyWithTimeline(Timeline timeline) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, @@ -236,7 +226,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public PlaybackInfo copyWithPlaybackState(int playbackState) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, @@ -260,7 +249,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public PlaybackInfo copyWithIsLoading(boolean isLoading) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, @@ -286,7 +274,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, @@ -310,7 +297,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public PlaybackInfo copyWithLoadingMediaPeriodId(MediaPeriodId loadingMediaPeriodId) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, 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 0e19212afa..68a386d2de 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 @@ -324,6 +324,29 @@ public interface Player { */ interface EventListener { + /** + * Called when the timeline has been refreshed. + * + *

        Note that if the timeline has changed then a position discontinuity may also have + * occurred. For example, the current period index may have changed as a result of periods being + * added or removed from the timeline. This will not be reported via a separate call to + * {@link #onPositionDiscontinuity(int)}. + * + * @param timeline The latest timeline. Never null, but may be empty. + * @param reason The {@link TimelineChangeReason} responsible for this timeline change. + */ + @SuppressWarnings("deprecation") + default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) { + Object manifest = null; + if (timeline.getWindowCount() == 1) { + // Legacy behavior was to report the manifest for single window timelines only. + Timeline.Window window = new Timeline.Window(); + manifest = timeline.getWindow(0, window).manifest; + } + // Call deprecated version. + onTimelineChanged(timeline, manifest, reason); + } + /** * Called when the timeline and/or manifest has been refreshed. * @@ -335,7 +358,11 @@ public interface Player { * @param timeline The latest timeline. Never null, but may be empty. * @param manifest The latest manifest. May be null. * @param reason The {@link TimelineChangeReason} responsible for this timeline change. + * @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be + * accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex, + * window).manifest} for a given window index. */ + @Deprecated default void onTimelineChanged( Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {} @@ -396,8 +423,7 @@ public interface Player { * when the source introduces a discontinuity internally). * *

        When a position discontinuity occurs as a result of a change to the timeline this method - * is not called. {@link #onTimelineChanged(Timeline, Object, int)} is called in this - * case. + * is not called. {@link #onTimelineChanged(Timeline, int)} is called in this case. * * @param reason The {@link DiscontinuityReason} responsible for the discontinuity. */ @@ -428,6 +454,19 @@ public interface Player { @Deprecated abstract class DefaultEventListener implements EventListener { + @Override + @SuppressWarnings("deprecation") + public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) { + Object manifest = null; + if (timeline.getWindowCount() == 1) { + // Legacy behavior was to report the manifest for single window timelines only. + Timeline.Window window = new Timeline.Window(); + manifest = timeline.getWindow(0, window).manifest; + } + // Call deprecated version. + onTimelineChanged(timeline, manifest, reason); + } + @Override @SuppressWarnings("deprecation") public void onTimelineChanged( @@ -436,7 +475,7 @@ public interface Player { onTimelineChanged(timeline, manifest); } - /** @deprecated Use {@link EventListener#onTimelineChanged(Timeline, Object, int)} instead. */ + /** @deprecated Use {@link EventListener#onTimelineChanged(Timeline, int)} instead. */ @Deprecated public void onTimelineChanged(Timeline timeline, @Nullable Object manifest) { // Do nothing. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index b427991d6e..a782255cb8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -1070,13 +1070,6 @@ public class SimpleExoPlayer extends BasePlayer return player.getCurrentTimeline(); } - @Override - @Nullable - public Object getCurrentManifest() { - verifyApplicationThread(); - return player.getCurrentManifest(); - } - @Override public int getCurrentPeriodIndex() { verifyApplicationThread(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 0c64810d58..32fa3a6e4b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -122,6 +122,9 @@ public abstract class Timeline { /** A tag for the window. Not necessarily unique. */ @Nullable public Object tag; + /** The manifest of the window. May be {@code null}. */ + @Nullable public Object manifest; + /** * The start time of the presentation to which this window belongs in milliseconds since the * epoch, or {@link C#TIME_UNSET} if unknown or not applicable. For informational purposes only. @@ -179,6 +182,7 @@ public abstract class Timeline { /** Sets the data held by this window. */ public Window set( @Nullable Object tag, + @Nullable Object manifest, long presentationStartTimeMs, long windowStartTimeMs, boolean isSeekable, @@ -189,6 +193,7 @@ public abstract class Timeline { int lastPeriodIndex, long positionInFirstPeriodUs) { this.tag = tag; + this.manifest = manifest; this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; this.isSeekable = isSeekable; 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 deecfb15a8..de0f177342 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 @@ -437,8 +437,7 @@ public class AnalyticsCollector // having slightly different real times. @Override - public final void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { + public final void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { mediaPeriodQueueTracker.onTimelineChanged(timeline); EventTime eventTime = generatePlayingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 4858eec6b7..17bc304db3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -335,6 +335,7 @@ public final class DownloadHelper { private final RendererCapabilities[] rendererCapabilities; private final SparseIntArray scratchSet; private final Handler callbackHandler; + private final Timeline.Window window; private boolean isPreparedWithMedia; private @MonotonicNonNull Callback callback; @@ -374,6 +375,7 @@ public final class DownloadHelper { trackSelector.setParameters(trackSelectorParameters); trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter()); callbackHandler = new Handler(Util.getLooper()); + window = new Timeline.Window(); } /** @@ -409,7 +411,9 @@ public final class DownloadHelper { return null; } assertPreparedWithMedia(); - return mediaPreparer.manifest; + return mediaPreparer.timeline.getWindowCount() > 0 + ? mediaPreparer.timeline.getWindow(/* windowIndex= */ 0, window).manifest + : null; } /** @@ -814,7 +818,6 @@ public final class DownloadHelper { private final HandlerThread mediaSourceThread; private final Handler mediaSourceHandler; - @Nullable public Object manifest; public @MonotonicNonNull Timeline timeline; public MediaPeriod @MonotonicNonNull [] mediaPeriods; @@ -892,14 +895,12 @@ public final class DownloadHelper { // MediaSource.SourceInfoRefreshListener implementation. @Override - public void onSourceInfoRefreshed( - MediaSource source, Timeline timeline, @Nullable Object manifest) { + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { if (this.timeline != null) { // Ignore dynamic updates. return; } this.timeline = timeline; - this.manifest = manifest; mediaPeriods = new MediaPeriod[timeline.getPeriodCount()]; for (int i = 0; i < mediaPeriods.length; i++) { MediaPeriod mediaPeriod = diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java index f6ea3da089..124f70c64c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java @@ -27,8 +27,8 @@ import java.util.ArrayList; * Base {@link MediaSource} implementation to handle parallel reuse and to keep a list of {@link * MediaSourceEventListener}s. * - *

        Whenever an implementing subclass needs to provide a new timeline and/or manifest, it must - * call {@link #refreshSourceInfo(Timeline, Object)} to notify all listeners. + *

        Whenever an implementing subclass needs to provide a new timeline, it must call {@link + * #refreshSourceInfo(Timeline)} to notify all listeners. */ public abstract class BaseMediaSource implements MediaSource { @@ -37,7 +37,6 @@ public abstract class BaseMediaSource implements MediaSource { @Nullable private Looper looper; @Nullable private Timeline timeline; - @Nullable private Object manifest; public BaseMediaSource() { sourceInfoListeners = new ArrayList<>(/* initialCapacity= */ 1); @@ -65,13 +64,11 @@ public abstract class BaseMediaSource implements MediaSource { * Updates timeline and manifest and notifies all listeners of the update. * * @param timeline The new {@link Timeline}. - * @param manifest The new manifest. May be null. */ - protected final void refreshSourceInfo(Timeline timeline, @Nullable Object manifest) { + protected final void refreshSourceInfo(Timeline timeline) { this.timeline = timeline; - this.manifest = manifest; for (SourceInfoRefreshListener listener : sourceInfoListeners) { - listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest); + listener.onSourceInfoRefreshed(/* source= */ this, timeline); } } @@ -139,7 +136,7 @@ public abstract class BaseMediaSource implements MediaSource { this.looper = looper; prepareSourceInternal(mediaTransferListener); } else if (timeline != null) { - listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest); + listener.onSourceInfoRefreshed(/* source= */ this, timeline); } } @@ -149,7 +146,6 @@ public abstract class BaseMediaSource implements MediaSource { if (sourceInfoListeners.isEmpty()) { looper = null; timeline = null; - manifest = null; releaseSourceInternal(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index c942f9320e..81169354de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -87,7 +87,6 @@ public final class ClippingMediaSource extends CompositeMediaSource { private final ArrayList mediaPeriods; private final Timeline.Window window; - @Nullable private Object manifest; @Nullable private ClippingTimeline clippingTimeline; @Nullable private IllegalClippingException clippingError; private long periodStartUs; @@ -235,12 +234,10 @@ public final class ClippingMediaSource extends CompositeMediaSource { } @Override - protected void onChildSourceInfoRefreshed( - Void id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) { + protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline timeline) { if (clippingError != null) { return; } - this.manifest = manifest; refreshClippedTimeline(timeline); } @@ -280,7 +277,7 @@ public final class ClippingMediaSource extends CompositeMediaSource { clippingError = e; return; } - refreshSourceInfo(clippingTimeline, manifest); + refreshSourceInfo(clippingTimeline); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java index 1a9e1ff250..612ad33f9d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java @@ -73,17 +73,15 @@ public abstract class CompositeMediaSource extends BaseMediaSource { * @param id The unique id used to prepare the child source. * @param mediaSource The child source whose source info has been refreshed. * @param timeline The timeline of the child source. - * @param manifest The manifest of the child source. */ protected abstract void onChildSourceInfoRefreshed( - T id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest); + T id, MediaSource mediaSource, Timeline timeline); /** * Prepares a child source. * - *

        {@link #onChildSourceInfoRefreshed(Object, MediaSource, Timeline, Object)} will be called - * when the child source updates its timeline and/or manifest with the same {@code id} passed to - * this method. + *

        {@link #onChildSourceInfoRefreshed(Object, MediaSource, Timeline)} will be called when the + * child source updates its timeline with the same {@code id} passed to this method. * *

        Any child sources that aren't explicitly released with {@link #releaseChildSource(Object)} * will be released in {@link #releaseSourceInternal()}. @@ -94,7 +92,12 @@ public abstract class CompositeMediaSource extends BaseMediaSource { protected final void prepareChildSource(final T id, MediaSource mediaSource) { Assertions.checkArgument(!childSources.containsKey(id)); SourceInfoRefreshListener sourceListener = - (source, timeline, manifest) -> onChildSourceInfoRefreshed(id, source, timeline, manifest); + new SourceInfoRefreshListener() { + @Override + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { + onChildSourceInfoRefreshed(id, source, timeline); + } + }; MediaSourceEventListener eventListener = new ForwardingEventListener(id); childSources.put(id, new MediaSourceAndListener(mediaSource, sourceListener, eventListener)); mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener); 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 c72bed1b5b..18d5c49fb4 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 @@ -474,10 +474,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource onCompletionActions = nextTimelineUpdateOnCompletionActions; nextTimelineUpdateOnCompletionActions = new HashSet<>(); - refreshSourceInfo( - new ConcatenatedTimeline(mediaSourceHolders, shuffleOrder, isAtomic), /* manifest= */ null); + refreshSourceInfo(new ConcatenatedTimeline(mediaSourceHolders, shuffleOrder, isAtomic)); getPlaybackThreadHandlerOnPlaybackThread() .obtainMessage(MSG_ON_COMPLETION, onCompletionActions) .sendToTarget(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index f07ee63e79..2bcaad4fce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -364,9 +364,8 @@ public final class ExtractorMediaSource extends BaseMediaSource } @Override - public void onSourceInfoRefreshed( - MediaSource source, Timeline timeline, @Nullable Object manifest) { - refreshSourceInfo(timeline, manifest); + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { + refreshSourceInfo(timeline); } @Deprecated diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 7adb18dc94..ac23e2a831 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -100,13 +100,12 @@ public final class LoopingMediaSource extends CompositeMediaSource { } @Override - protected void onChildSourceInfoRefreshed( - Void id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) { + protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline timeline) { Timeline loopingTimeline = loopCount != Integer.MAX_VALUE ? new LoopingTimeline(timeline, loopCount) : new InfinitelyLoopingTimeline(timeline); - refreshSourceInfo(loopingTimeline, manifest); + refreshSourceInfo(loopingTimeline); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java index ad9ef194da..1fca824910 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java @@ -119,7 +119,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { @Override protected void onChildSourceInfoRefreshed( - Void id, MediaSource mediaSource, Timeline newTimeline, @Nullable Object manifest) { + Void id, MediaSource mediaSource, Timeline newTimeline) { if (isPrepared) { timeline = timeline.cloneWithUpdatedTimeline(newTimeline); } else if (newTimeline.isEmpty()) { @@ -162,7 +162,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { } } isPrepared = true; - refreshSourceInfo(this.timeline, manifest); + refreshSourceInfo(this.timeline); } @Nullable @@ -274,6 +274,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { return window.set( tag, + /* manifest= */ null, /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, /* isSeekable= */ false, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index b40bbb35d1..f86be8afc2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -58,8 +58,8 @@ public interface MediaPeriod extends SequenceableLoader { * *

        If preparation succeeds and results in a source timeline change (e.g. the period duration * becoming known), {@link - * MediaSource.SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} - * will be called before {@code callback.onPrepared}. + * MediaSource.SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline)} will be + * called before {@code callback.onPrepared}. * * @param callback Callback to receive updates from this period, including being notified when * preparation completes. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 82359ffccd..10e29f3f44 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -49,16 +49,16 @@ public interface MediaSource { interface SourceInfoRefreshListener { /** - * Called when manifest and/or timeline has been refreshed. - *

        - * Called on the playback thread. + * Called when the timeline has been refreshed. + * + *

        Called on the playback thread. * * @param source The {@link MediaSource} whose info has been refreshed. * @param timeline The source's timeline. - * @param manifest The loaded manifest. May be null. */ - void onSourceInfoRefreshed(MediaSource source, Timeline timeline, @Nullable Object manifest); - + default void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { + // Do nothing. + } } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index f12ce92f54..dd7675f3d4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -71,7 +71,6 @@ public final class MergingMediaSource extends CompositeMediaSource { private final ArrayList pendingTimelineSources; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - @Nullable private Object primaryManifest; private int periodCount; @Nullable private IllegalMergeException mergeError; @@ -143,7 +142,6 @@ public final class MergingMediaSource extends CompositeMediaSource { protected void releaseSourceInternal() { super.releaseSourceInternal(); Arrays.fill(timelines, null); - primaryManifest = null; periodCount = PERIOD_COUNT_UNSET; mergeError = null; pendingTimelineSources.clear(); @@ -152,7 +150,7 @@ public final class MergingMediaSource extends CompositeMediaSource { @Override protected void onChildSourceInfoRefreshed( - Integer id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) { + Integer id, MediaSource mediaSource, Timeline timeline) { if (mergeError == null) { mergeError = checkTimelineMerges(timeline); } @@ -161,11 +159,8 @@ public final class MergingMediaSource extends CompositeMediaSource { } pendingTimelineSources.remove(mediaSource); timelines[id] = timeline; - if (mediaSource == mediaSources[0]) { - primaryManifest = manifest; - } if (pendingTimelineSources.isEmpty()) { - refreshSourceInfo(timelines[0], primaryManifest); + refreshSourceInfo(timelines[0]); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java index ba69b46d7f..42ec237b3e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -287,7 +287,10 @@ public final class ProgressiveMediaSource extends BaseMediaSource // TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223. refreshSourceInfo( new SinglePeriodTimeline( - timelineDurationUs, timelineIsSeekable, /* isDynamic= */ false, tag), - /* manifest= */ null); + timelineDurationUs, + timelineIsSeekable, + /* isDynamic= */ false, + /* manifest= */ null, + tag)); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java index fc99e8cb7b..a5b78ef3f7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java @@ -68,8 +68,7 @@ public final class SilenceMediaSource extends BaseMediaSource { @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { refreshSourceInfo( - new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false), - /* manifest= */ null); + new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false)); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index 14648775f8..8790b09f07 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -36,6 +36,7 @@ public final class SinglePeriodTimeline extends Timeline { private final boolean isSeekable; private final boolean isDynamic; @Nullable private final Object tag; + @Nullable private final Object manifest; /** * Creates a timeline containing a single period and a window that spans it. @@ -45,7 +46,7 @@ public final class SinglePeriodTimeline extends Timeline { * @param isDynamic Whether the window may change when the timeline is updated. */ public SinglePeriodTimeline(long durationUs, boolean isSeekable, boolean isDynamic) { - this(durationUs, isSeekable, isDynamic, /* tag= */ null); + this(durationUs, isSeekable, isDynamic, /* manifest= */ null, /* tag= */ null); } /** @@ -54,10 +55,15 @@ public final class SinglePeriodTimeline extends Timeline { * @param durationUs The duration of the period, in microseconds. * @param isSeekable Whether seeking is supported within the period. * @param isDynamic Whether the window may change when the timeline is updated. - * @param tag A tag used for {@link Timeline.Window#tag}. + * @param manifest The manifest. May be {@code null}. + * @param tag A tag used for {@link Window#tag}. */ public SinglePeriodTimeline( - long durationUs, boolean isSeekable, boolean isDynamic, @Nullable Object tag) { + long durationUs, + boolean isSeekable, + boolean isDynamic, + @Nullable Object manifest, + @Nullable Object tag) { this( durationUs, durationUs, @@ -65,6 +71,7 @@ public final class SinglePeriodTimeline extends Timeline { /* windowDefaultStartPositionUs= */ 0, isSeekable, isDynamic, + manifest, tag); } @@ -80,6 +87,7 @@ public final class SinglePeriodTimeline extends Timeline { * which to begin playback, in microseconds. * @param isSeekable Whether seeking is supported within the window. * @param isDynamic Whether the window may change when the timeline is updated. + * @param manifest The manifest. May be (@code null}. * @param tag A tag used for {@link Timeline.Window#tag}. */ public SinglePeriodTimeline( @@ -89,6 +97,7 @@ public final class SinglePeriodTimeline extends Timeline { long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic, + @Nullable Object manifest, @Nullable Object tag) { this( /* presentationStartTimeMs= */ C.TIME_UNSET, @@ -99,6 +108,7 @@ public final class SinglePeriodTimeline extends Timeline { windowDefaultStartPositionUs, isSeekable, isDynamic, + manifest, tag); } @@ -117,6 +127,7 @@ public final class SinglePeriodTimeline extends Timeline { * which to begin playback, in microseconds. * @param isSeekable Whether seeking is supported within the window. * @param isDynamic Whether the window may change when the timeline is updated. + * @param manifest The manifest. May be {@code null}. * @param tag A tag used for {@link Timeline.Window#tag}. */ public SinglePeriodTimeline( @@ -128,6 +139,7 @@ public final class SinglePeriodTimeline extends Timeline { long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic, + @Nullable Object manifest, @Nullable Object tag) { this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; @@ -137,6 +149,7 @@ public final class SinglePeriodTimeline extends Timeline { this.windowDefaultStartPositionUs = windowDefaultStartPositionUs; this.isSeekable = isSeekable; this.isDynamic = isDynamic; + this.manifest = manifest; this.tag = tag; } @@ -165,6 +178,7 @@ public final class SinglePeriodTimeline extends Timeline { } return window.set( tag, + manifest, presentationStartTimeMs, windowStartTimeMs, isSeekable, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 6c1881a01a..04ee3a153c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -290,7 +290,8 @@ public final class SingleSampleMediaSource extends BaseMediaSource { this.tag = tag; dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP); timeline = - new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false, tag); + new SinglePeriodTimeline( + durationUs, /* isSeekable= */ true, /* isDynamic= */ false, /* manifest= */ null, tag); } // MediaSource implementation. @@ -304,7 +305,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { transferListener = mediaTransferListener; - refreshSourceInfo(timeline, /* manifest= */ null); + refreshSourceInfo(timeline); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index a6c2cf2767..5e22de4320 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -134,7 +134,6 @@ public final class AdsMediaSource extends CompositeMediaSource { // Accessed on the player thread. @Nullable private ComponentListener componentListener; @Nullable private Timeline contentTimeline; - @Nullable private Object contentManifest; @Nullable private AdPlaybackState adPlaybackState; private @NullableType MediaSource[][] adGroupMediaSources; private @NullableType Timeline[][] adGroupTimelines; @@ -265,7 +264,6 @@ public final class AdsMediaSource extends CompositeMediaSource { componentListener = null; maskingMediaPeriodByAdMediaSource.clear(); contentTimeline = null; - contentManifest = null; adPlaybackState = null; adGroupMediaSources = new MediaSource[0][]; adGroupTimelines = new Timeline[0][]; @@ -274,16 +272,13 @@ public final class AdsMediaSource extends CompositeMediaSource { @Override protected void onChildSourceInfoRefreshed( - MediaPeriodId mediaPeriodId, - MediaSource mediaSource, - Timeline timeline, - @Nullable Object manifest) { + MediaPeriodId mediaPeriodId, MediaSource mediaSource, Timeline timeline) { if (mediaPeriodId.isAd()) { int adGroupIndex = mediaPeriodId.adGroupIndex; int adIndexInAdGroup = mediaPeriodId.adIndexInAdGroup; onAdSourceInfoRefreshed(mediaSource, adGroupIndex, adIndexInAdGroup, timeline); } else { - onContentSourceInfoRefreshed(timeline, manifest); + onContentSourceInfoRefreshed(timeline); } } @@ -308,10 +303,9 @@ public final class AdsMediaSource extends CompositeMediaSource { maybeUpdateSourceInfo(); } - private void onContentSourceInfoRefreshed(Timeline timeline, @Nullable Object manifest) { + private void onContentSourceInfoRefreshed(Timeline timeline) { Assertions.checkArgument(timeline.getPeriodCount() == 1); contentTimeline = timeline; - contentManifest = manifest; maybeUpdateSourceInfo(); } @@ -340,7 +334,7 @@ public final class AdsMediaSource extends CompositeMediaSource { adPlaybackState.adGroupCount == 0 ? contentTimeline : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState); - refreshSourceInfo(timeline, contentManifest); + refreshSourceInfo(timeline); } } 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 639e80348b..61b8418411 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 @@ -113,8 +113,8 @@ public final class ExoPlayerTest { /** Tests playback of a source that exposes a single period. */ @Test public void testPlaySinglePeriodTimeline() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); Object manifest = new Object(); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1, manifest); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ExoPlayerTestRunner testRunner = new Builder() @@ -126,7 +126,6 @@ public final class ExoPlayerTest { .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(timeline); - testRunner.assertManifestsEqual(manifest); testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); assertThat(renderer.formatReadCount).isEqualTo(1); @@ -256,15 +255,16 @@ public final class ExoPlayerTest { @Test public void testRepreparationGivesFreshSourceInfo() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); Object firstSourceManifest = new Object(); - MediaSource firstSource = - new FakeMediaSource(timeline, firstSourceManifest, Builder.VIDEO_FORMAT); + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, firstSourceManifest); + MediaSource firstSource = new FakeMediaSource(firstTimeline, Builder.VIDEO_FORMAT); final CountDownLatch queuedSourceInfoCountDownLatch = new CountDownLatch(1); final CountDownLatch completePreparationCountDownLatch = new CountDownLatch(1); + + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); MediaSource secondSource = - new FakeMediaSource(timeline, new Object(), Builder.VIDEO_FORMAT) { + new FakeMediaSource(secondTimeline, Builder.VIDEO_FORMAT) { @Override public synchronized void prepareSourceInternal( @Nullable TransferListener mediaTransferListener) { @@ -281,8 +281,8 @@ public final class ExoPlayerTest { } }; Object thirdSourceManifest = new Object(); - MediaSource thirdSource = - new FakeMediaSource(timeline, thirdSourceManifest, Builder.VIDEO_FORMAT); + Timeline thirdTimeline = new FakeTimeline(/* windowCount= */ 1, thirdSourceManifest); + MediaSource thirdSource = new FakeMediaSource(thirdTimeline, Builder.VIDEO_FORMAT); // Prepare the player with a source with the first manifest and a non-empty timeline. Prepare // the player again with a source and a new manifest, which will never be exposed. Allow the @@ -290,7 +290,7 @@ public final class ExoPlayerTest { // the test thread's call to prepare() has returned. ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparation") - .waitForTimelineChanged(timeline) + .waitForTimelineChanged(firstTimeline) .prepareSource(secondSource) .executeRunnable( () -> { @@ -315,8 +315,7 @@ public final class ExoPlayerTest { // The first source's preparation completed with a non-empty timeline. When the player was // re-prepared with the second source, it immediately exposed an empty timeline, but the source // info refresh from the second source was suppressed as we re-prepared with the third source. - testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline); - testRunner.assertManifestsEqual(firstSourceManifest, null, thirdSourceManifest); + testRunner.assertTimelinesEqual(firstTimeline, Timeline.EMPTY, thirdTimeline); testRunner.assertTimelineChangeReasonsEqual( Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_RESET, @@ -376,9 +375,9 @@ public final class ExoPlayerTest { public void testShuffleModeEnabledChanges() throws Exception { Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); MediaSource[] fakeMediaSources = { - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT) + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT) }; ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(false, new FakeShuffleOrder(3), fakeMediaSources); @@ -437,8 +436,7 @@ public final class ExoPlayerTest { /* isDynamic= */ false, /* durationUs= */ C.MICROS_PER_SECOND, errorAdPlaybackState)); - final FakeMediaSource fakeMediaSource = - new FakeMediaSource(fakeTimeline, /* manifest= */ null, Builder.VIDEO_FORMAT); + final FakeMediaSource fakeMediaSource = new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testAdGroupWithLoadErrorIsSkipped") .pause() @@ -585,7 +583,7 @@ public final class ExoPlayerTest { public void testSeekDiscontinuityWithAdjustment() throws Exception { FakeTimeline timeline = new FakeTimeline(1); FakeMediaSource mediaSource = - new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { + new FakeMediaSource(timeline, Builder.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod( MediaPeriodId id, @@ -620,7 +618,7 @@ public final class ExoPlayerTest { public void testInternalDiscontinuityAtNewPosition() throws Exception { FakeTimeline timeline = new FakeTimeline(1); FakeMediaSource mediaSource = - new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { + new FakeMediaSource(timeline, Builder.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod( MediaPeriodId id, @@ -646,7 +644,7 @@ public final class ExoPlayerTest { public void testInternalDiscontinuityAtInitialPosition() throws Exception { FakeTimeline timeline = new FakeTimeline(1); FakeMediaSource mediaSource = - new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { + new FakeMediaSource(timeline, Builder.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod( MediaPeriodId id, @@ -673,7 +671,7 @@ public final class ExoPlayerTest { public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = - new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + new FakeMediaSource(timeline, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); FakeTrackSelector trackSelector = new FakeTrackSelector(); @@ -702,7 +700,7 @@ public final class ExoPlayerTest { public void testAllActivatedTrackSelectionAreReleasedForMultiPeriods() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 2); MediaSource mediaSource = - new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + new FakeMediaSource(timeline, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); FakeTrackSelector trackSelector = new FakeTrackSelector(); @@ -732,7 +730,7 @@ public final class ExoPlayerTest { throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = - new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + new FakeMediaSource(timeline, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); final FakeTrackSelector trackSelector = new FakeTrackSelector(); @@ -771,7 +769,7 @@ public final class ExoPlayerTest { throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = - new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + new FakeMediaSource(timeline, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); final FakeTrackSelector trackSelector = new FakeTrackSelector(/* reuse track selection */ true); @@ -810,7 +808,7 @@ public final class ExoPlayerTest { public void testDynamicTimelineChangeReason() throws Exception { Timeline timeline1 = new FakeTimeline(new TimelineWindowDefinition(false, false, 100000)); final Timeline timeline2 = new FakeTimeline(new TimelineWindowDefinition(false, false, 20000)); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline1, null, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline1, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testDynamicTimelineChangeReason") .pause() @@ -841,14 +839,14 @@ public final class ExoPlayerTest { new ConcatenatingMediaSource( /* isAtomic= */ false, new FakeShuffleOrder(/* length= */ 2), - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT)); + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); ConcatenatingMediaSource secondMediaSource = new ConcatenatingMediaSource( /* isAtomic= */ false, new FakeShuffleOrder(/* length= */ 2), - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT)); + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparationWithShuffle") // Wait for first preparation and enable shuffling. Plays period 0. @@ -877,7 +875,7 @@ public final class ExoPlayerTest { final CountDownLatch createPeriodCalledCountDownLatch = new CountDownLatch(1); final FakeMediaPeriod[] fakeMediaPeriodHolder = new FakeMediaPeriod[1]; MediaSource mediaSource = - new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), null, Builder.VIDEO_FORMAT) { + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), Builder.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod( MediaPeriodId id, @@ -1017,8 +1015,7 @@ public final class ExoPlayerTest { @Test public void testStopWithoutResetReleasesMediaSource() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final FakeMediaSource mediaSource = - new FakeMediaSource(timeline, /* manifest= */ null, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopReleasesMediaSource") .waitForPlaybackState(Player.STATE_READY) @@ -1038,8 +1035,7 @@ public final class ExoPlayerTest { @Test public void testStopWithResetReleasesMediaSource() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final FakeMediaSource mediaSource = - new FakeMediaSource(timeline, /* manifest= */ null, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopReleasesMediaSource") .waitForPlaybackState(Player.STATE_READY) @@ -1059,7 +1055,7 @@ public final class ExoPlayerTest { @Test public void testRepreparationDoesNotResetAfterStopWithReset() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - MediaSource secondSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + MediaSource secondSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparationAfterStop") .waitForPlaybackState(Player.STATE_READY) @@ -1087,7 +1083,7 @@ public final class ExoPlayerTest { public void testSeekBeforeRepreparationPossibleAfterStopWithReset() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2); - MediaSource secondSource = new FakeMediaSource(secondTimeline, null, Builder.VIDEO_FORMAT); + MediaSource secondSource = new FakeMediaSource(secondTimeline, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekAfterStopWithReset") .waitForPlaybackState(Player.STATE_READY) @@ -1122,7 +1118,7 @@ public final class ExoPlayerTest { Timeline secondTimeline = new FakeTimeline( new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ new Object())); - MediaSource secondSource = new FakeMediaSource(secondTimeline, /* manifest= */ null); + MediaSource secondSource = new FakeMediaSource(secondTimeline); AtomicLong positionAfterReprepare = new AtomicLong(); ActionSchedule actionSchedule = new ActionSchedule.Builder("testReprepareAndKeepPositionWithNewMediaSource") @@ -1211,9 +1207,7 @@ public final class ExoPlayerTest { .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) .prepareSource( - new FakeMediaSource(timeline, /* manifest= */ null), - /* resetPosition= */ true, - /* resetState= */ false) + new FakeMediaSource(timeline), /* resetPosition= */ true, /* resetState= */ false) .waitForPlaybackState(Player.STATE_READY) .build(); ExoPlayerTestRunner testRunner = @@ -1252,9 +1246,7 @@ public final class ExoPlayerTest { } }) .prepareSource( - new FakeMediaSource(timeline, /* manifest= */ null), - /* resetPosition= */ false, - /* resetState= */ false) + new FakeMediaSource(timeline), /* resetPosition= */ false, /* resetState= */ false) .waitForPlaybackState(Player.STATE_READY) .executeRunnable( new PlayerRunnable() { @@ -1287,8 +1279,7 @@ public final class ExoPlayerTest { @Test public void testInvalidSeekPositionAfterSourceInfoRefreshStillUpdatesTimeline() throws Exception { final Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final FakeMediaSource mediaSource = - new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); + final FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null); ActionSchedule actionSchedule = new ActionSchedule.Builder("testInvalidSeekPositionSourceInfoRefreshStillUpdatesTimeline") .waitForPlaybackState(Player.STATE_BUFFERING) @@ -1312,8 +1303,7 @@ public final class ExoPlayerTest { public void testInvalidSeekPositionAfterSourceInfoRefreshWithShuffleModeEnabledUsesCorrectFirstPeriod() throws Exception { - FakeMediaSource mediaSource = - new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null); + FakeMediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)); ConcatenatingMediaSource concatenatingMediaSource = new ConcatenatingMediaSource( /* isAtomic= */ false, new FakeShuffleOrder(0), mediaSource, mediaSource); @@ -1347,7 +1337,7 @@ public final class ExoPlayerTest { public void testRestartAfterEmptyTimelineWithShuffleModeEnabledUsesCorrectFirstPeriod() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - FakeMediaSource mediaSource = new FakeMediaSource(timeline, /* manifest= */ null); + FakeMediaSource mediaSource = new FakeMediaSource(timeline); ConcatenatingMediaSource concatenatingMediaSource = new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); AtomicInteger windowIndexAfterAddingSources = new AtomicInteger(); @@ -1386,8 +1376,7 @@ public final class ExoPlayerTest { final Timeline timeline = new FakeTimeline(/* windowCount= */ 2); final long[] positionHolder = new long[3]; final int[] windowIndexHolder = new int[3]; - final FakeMediaSource secondMediaSource = - new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); + final FakeMediaSource secondMediaSource = new FakeMediaSource(/* timeline= */ null); ActionSchedule actionSchedule = new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition") .pause() @@ -1450,8 +1439,7 @@ public final class ExoPlayerTest { @Test public void testPlaybackErrorTwiceStillKeepsTimeline() throws Exception { final Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final FakeMediaSource mediaSource2 = - new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); + final FakeMediaSource mediaSource2 = new FakeMediaSource(/* timeline= */ null); ActionSchedule actionSchedule = new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition") .pause() @@ -1609,9 +1597,7 @@ public final class ExoPlayerTest { // messages sent at end of playback are received before test ends. .waitForPlaybackState(Player.STATE_ENDED) .prepareSource( - new FakeMediaSource(timeline, null), - /* resetPosition= */ false, - /* resetState= */ true) + new FakeMediaSource(timeline), /* resetPosition= */ false, /* resetState= */ true) .waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_ENDED) .build(); @@ -1774,7 +1760,7 @@ public final class ExoPlayerTest { new FakeTimeline( new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") @@ -1847,7 +1833,7 @@ public final class ExoPlayerTest { new FakeTimeline( new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") @@ -1873,9 +1859,9 @@ public final class ExoPlayerTest { public void testSendMessagesNonLinearPeriodOrder() throws Exception { Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); MediaSource[] fakeMediaSources = { - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT) + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT) }; ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(false, new FakeShuffleOrder(3), fakeMediaSources); @@ -2022,8 +2008,7 @@ public final class ExoPlayerTest { new FakeTimeline( new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 3)); - final FakeMediaSource mediaSource = - new FakeMediaSource(timeline1, /* manifest= */ null, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline1, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testTimelineUpdateDropsPeriods") .pause() @@ -2069,7 +2054,7 @@ public final class ExoPlayerTest { /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10 * C.MICROS_PER_SECOND)); - FakeMediaSource mediaSource = new FakeMediaSource(timeline, /* manifest= */ null); + FakeMediaSource mediaSource = new FakeMediaSource(timeline); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekToUnpreparedPeriod") .pause() @@ -2163,7 +2148,7 @@ public final class ExoPlayerTest { final EventListener eventListener = new EventListener() { @Override - public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) { + public void onTimelineChanged(Timeline timeline, int reason) { if (timeline.isEmpty()) { playerReference.get().setPlayWhenReady(/* playWhenReady= */ false); } @@ -2208,7 +2193,7 @@ public final class ExoPlayerTest { long expectedDurationUs = 700_000; MediaSource mediaSource = new ClippingMediaSource( - new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null), + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), startPositionUs, startPositionUs + expectedDurationUs); Clock clock = new AutoAdvancingFakeClock(); @@ -2274,8 +2259,8 @@ public final class ExoPlayerTest { new TimelineWindowDefinition( /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ C.TIME_UNSET)); MediaSource[] fakeMediaSources = { - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, null, Builder.AUDIO_FORMAT) + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, Builder.AUDIO_FORMAT) }; MediaSource mediaSource = new ConcatenatingMediaSource(fakeMediaSources); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); @@ -2326,10 +2311,9 @@ public final class ExoPlayerTest { /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10 * C.MICROS_PER_SECOND)); - MediaSource workingMediaSource = - new FakeMediaSource(fakeTimeline, /* manifest= */ null, Builder.VIDEO_FORMAT); + MediaSource workingMediaSource = new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT); MediaSource failingMediaSource = - new FakeMediaSource(/* timeline= */ null, /* manifest= */ null, Builder.VIDEO_FORMAT) { + new FakeMediaSource(/* timeline= */ null, Builder.VIDEO_FORMAT) { @Override public void maybeThrowSourceInfoRefreshError() throws IOException { throw new IOException(); @@ -2363,10 +2347,9 @@ public final class ExoPlayerTest { /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10 * C.MICROS_PER_SECOND)); - MediaSource workingMediaSource = - new FakeMediaSource(fakeTimeline, /* manifest= */ null, Builder.VIDEO_FORMAT); + MediaSource workingMediaSource = new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT); MediaSource failingMediaSource = - new FakeMediaSource(/* timeline= */ null, /* manifest= */ null, Builder.VIDEO_FORMAT) { + new FakeMediaSource(/* timeline= */ null, Builder.VIDEO_FORMAT) { @Override public void maybeThrowSourceInfoRefreshError() throws IOException { throw new IOException(); @@ -2408,7 +2391,7 @@ public final class ExoPlayerTest { /* durationUs= */ 10 * C.MICROS_PER_SECOND)); AtomicReference wasReadyOnce = new AtomicReference<>(false); MediaSource mediaSource = - new FakeMediaSource(fakeTimeline, /* manifest= */ null, Builder.VIDEO_FORMAT) { + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT) { @Override public void maybeThrowSourceInfoRefreshError() throws IOException { if (wasReadyOnce.get()) { @@ -2446,7 +2429,7 @@ public final class ExoPlayerTest { new FakeTimeline( new TimelineWindowDefinition( /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 100_000)); - MediaSource mediaSource = new FakeMediaSource(timeline, /* manifest= */ null); + MediaSource mediaSource = new FakeMediaSource(timeline); ConcatenatingMediaSource concatenatingMediaSource = new ConcatenatingMediaSource(mediaSource); ActionSchedule actionSchedule = new ActionSchedule.Builder("removingLoopingLastPeriodFromPlaylistDoesNotThrow") @@ -2471,7 +2454,7 @@ public final class ExoPlayerTest { public void seekToUnpreparedWindowWithNonZeroOffsetInConcatenationStartsAtCorrectPosition() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); + FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null); MediaSource clippedMediaSource = new ClippingMediaSource( mediaSource, @@ -2519,7 +2502,7 @@ public final class ExoPlayerTest { /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 2 * periodDurationMs * 1000)); - FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); + FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null); MediaSource concatenatedMediaSource = new ConcatenatingMediaSource(mediaSource); AtomicInteger periodIndexWhenReady = new AtomicInteger(); AtomicLong positionWhenReady = new AtomicLong(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java index 73f42c5fc9..def7f8552e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java @@ -353,7 +353,6 @@ public final class MediaPeriodQueueTest { playbackInfo = new PlaybackInfo( timeline, - /* manifest= */ null, mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, initialPositionUs), /* startPositionUs= */ 0, /* contentPositionUs= */ 0, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index 22aa63b83a..a2546adfe4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -125,7 +125,9 @@ public final class AnalyticsCollectorTest { public void testEmptyTimeline() throws Exception { FakeMediaSource mediaSource = new FakeMediaSource( - Timeline.EMPTY, /* manifest= */ null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + Timeline.EMPTY, + ExoPlayerTestRunner.Builder.VIDEO_FORMAT, + ExoPlayerTestRunner.Builder.AUDIO_FORMAT); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) @@ -140,7 +142,6 @@ public final class AnalyticsCollectorTest { FakeMediaSource mediaSource = new FakeMediaSource( SINGLE_PERIOD_TIMELINE, - /* manifest= */ null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); @@ -183,12 +184,10 @@ public final class AnalyticsCollectorTest { new ConcatenatingMediaSource( new FakeMediaSource( SINGLE_PERIOD_TIMELINE, - /* manifest= */ null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT), new FakeMediaSource( SINGLE_PERIOD_TIMELINE, - /* manifest= */ null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); @@ -242,9 +241,8 @@ public final class AnalyticsCollectorTest { public void testPeriodTransitionWithRendererChange() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT), - new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.AUDIO_FORMAT)); + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT), + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT)); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); populateEventIds(listener.lastReportedTimeline); @@ -296,9 +294,8 @@ public final class AnalyticsCollectorTest { public void testSeekToOtherPeriod() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT), - new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.AUDIO_FORMAT)); + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT), + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT)); ActionSchedule actionSchedule = new ActionSchedule.Builder("AnalyticsCollectorTest") .pause() @@ -361,12 +358,9 @@ public final class AnalyticsCollectorTest { public void testSeekBackAfterReadingAhead() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT), + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT), new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, - /* manifest= */ null, - Builder.VIDEO_FORMAT, - Builder.AUDIO_FORMAT)); + SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)); long periodDurationMs = SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs(); ActionSchedule actionSchedule = @@ -443,10 +437,8 @@ public final class AnalyticsCollectorTest { @Test public void testPrepareNewSource() throws Exception { - MediaSource mediaSource1 = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT); - MediaSource mediaSource2 = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT); + MediaSource mediaSource1 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); + MediaSource mediaSource2 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("AnalyticsCollectorTest") .pause() @@ -507,8 +499,7 @@ public final class AnalyticsCollectorTest { @Test public void testReprepareAfterError() throws Exception { - MediaSource mediaSource = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT); + MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("AnalyticsCollectorTest") .waitForPlaybackState(Player.STATE_READY) @@ -570,7 +561,7 @@ public final class AnalyticsCollectorTest { @Test public void testDynamicTimelineChange() throws Exception { MediaSource childMediaSource = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT); + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); final ConcatenatingMediaSource concatenatedMediaSource = new ConcatenatingMediaSource(childMediaSource, childMediaSource); long periodDurationMs = @@ -639,7 +630,7 @@ public final class AnalyticsCollectorTest { @Test public void testNotifyExternalEvents() throws Exception { - MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null); + MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE); ActionSchedule actionSchedule = new ActionSchedule.Builder("AnalyticsCollectorTest") .pause() diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java index 3b78a2e3ae..479936b82f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java @@ -60,9 +60,11 @@ public class DownloadHelperTest { private static final String TEST_DOWNLOAD_TYPE = "downloadType"; private static final String TEST_CACHE_KEY = "cacheKey"; - private static final Timeline TEST_TIMELINE = - new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ new Object())); private static final Object TEST_MANIFEST = new Object(); + private static final Timeline TEST_TIMELINE = + new FakeTimeline( + new Object[] {TEST_MANIFEST}, + new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ new Object())); private static final Format VIDEO_FORMAT_LOW = createVideoFormat(/* bitrate= */ 200_000); private static final Format VIDEO_FORMAT_HIGH = createVideoFormat(/* bitrate= */ 800_000); @@ -491,7 +493,7 @@ public class DownloadHelperTest { private static final class TestMediaSource extends FakeMediaSource { public TestMediaSource() { - super(TEST_TIMELINE, TEST_MANIFEST); + super(TEST_TIMELINE); } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 846600f243..89acb3ec3e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -185,6 +185,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline clippedTimeline = getClippedTimeline(timeline, /* durationUs= */ TEST_CLIP_AMOUNT_US); @@ -206,6 +207,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline timeline2 = new SinglePeriodTimeline( @@ -215,6 +217,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline[] clippedTimelines = @@ -253,6 +256,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline timeline2 = new SinglePeriodTimeline( @@ -262,6 +266,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline[] clippedTimelines = @@ -300,6 +305,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline timeline2 = new SinglePeriodTimeline( @@ -309,6 +315,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline[] clippedTimelines = @@ -348,6 +355,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline timeline2 = new SinglePeriodTimeline( @@ -357,6 +365,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline[] clippedTimelines = @@ -473,7 +482,7 @@ public final class ClippingMediaSourceTest { new SinglePeriodTimeline( TEST_PERIOD_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false); FakeMediaSource fakeMediaSource = - new FakeMediaSource(timeline, /* manifest= */ null) { + new FakeMediaSource(timeline) { @Override protected FakeMediaPeriod createFakeMediaPeriod( MediaPeriodId id, @@ -530,7 +539,7 @@ public final class ClippingMediaSourceTest { */ private static Timeline getClippedTimeline(Timeline timeline, long startUs, long endUs) throws IOException { - FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, /* manifest= */ null); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline); ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startUs, endUs); return getClippedTimelines(fakeMediaSource, mediaSource)[0]; } @@ -540,7 +549,7 @@ public final class ClippingMediaSourceTest { */ private static Timeline getClippedTimeline(Timeline timeline, long durationUs) throws IOException { - FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, /* manifest= */ null); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline); ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, durationUs); return getClippedTimelines(fakeMediaSource, mediaSource)[0]; } @@ -557,7 +566,7 @@ public final class ClippingMediaSourceTest { Timeline firstTimeline, Timeline... additionalTimelines) throws IOException { - FakeMediaSource fakeMediaSource = new FakeMediaSource(firstTimeline, /* manifest= */ null); + FakeMediaSource fakeMediaSource = new FakeMediaSource(firstTimeline); ClippingMediaSource mediaSource = new ClippingMediaSource( fakeMediaSource, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 5187addec3..c587d85a85 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -226,7 +226,7 @@ public final class ConcatenatingMediaSourceTest { FakeMediaSource[] fastSources = createMediaSources(2); final FakeMediaSource[] lazySources = new FakeMediaSource[4]; for (int i = 0; i < 4; i++) { - lazySources[i] = new FakeMediaSource(null, null); + lazySources[i] = new FakeMediaSource(null); } // Add lazy sources and normal sources before preparation. Also remove one lazy source again @@ -307,16 +307,16 @@ public final class ConcatenatingMediaSourceTest { Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); - mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY, null)); + mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY)); timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertEmpty(timeline); mediaSource.addMediaSources( Arrays.asList( new MediaSource[] { - new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), - new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), - new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null) + new FakeMediaSource(Timeline.EMPTY), new FakeMediaSource(Timeline.EMPTY), + new FakeMediaSource(Timeline.EMPTY), new FakeMediaSource(Timeline.EMPTY), + new FakeMediaSource(Timeline.EMPTY), new FakeMediaSource(Timeline.EMPTY) })); timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertEmpty(timeline); @@ -362,9 +362,9 @@ public final class ConcatenatingMediaSourceTest { public void testDynamicChangeOfEmptyTimelines() throws IOException { FakeMediaSource[] childSources = new FakeMediaSource[] { - new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null), - new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null), - new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null), + new FakeMediaSource(Timeline.EMPTY), + new FakeMediaSource(Timeline.EMPTY), + new FakeMediaSource(Timeline.EMPTY), }; Timeline nonEmptyTimeline = new FakeTimeline(/* windowCount = */ 1); @@ -387,7 +387,7 @@ public final class ConcatenatingMediaSourceTest { @Test public void testIllegalArguments() { - MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null); + MediaSource validSource = new FakeMediaSource(createFakeTimeline(1)); // Null sources. try { @@ -660,8 +660,8 @@ public final class ConcatenatingMediaSourceTest { 10 * C.MICROS_PER_SECOND, FakeTimeline.createAdPlaybackState( /* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 0))); - FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); - FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); + FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly); + FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds); mediaSource.addMediaSource(mediaSourceContentOnly); mediaSource.addMediaSource(mediaSourceWithAds); @@ -807,7 +807,7 @@ public final class ConcatenatingMediaSourceTest { @Test public void testDuplicateMediaSources() throws IOException, InterruptedException { Timeline childTimeline = new FakeTimeline(/* windowCount= */ 2); - FakeMediaSource childSource = new FakeMediaSource(childTimeline, /* manifest= */ null); + FakeMediaSource childSource = new FakeMediaSource(childTimeline); mediaSource.addMediaSource(childSource); mediaSource.addMediaSource(childSource); @@ -840,7 +840,7 @@ public final class ConcatenatingMediaSourceTest { @Test public void testDuplicateNestedMediaSources() throws IOException, InterruptedException { Timeline childTimeline = new FakeTimeline(/* windowCount= */ 1); - FakeMediaSource childSource = new FakeMediaSource(childTimeline, /* manifest= */ null); + FakeMediaSource childSource = new FakeMediaSource(childTimeline); ConcatenatingMediaSource nestedConcatenation = new ConcatenatingMediaSource(); testRunner.prepareSource(); @@ -874,8 +874,7 @@ public final class ConcatenatingMediaSourceTest { public void testClear() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); final FakeMediaSource preparedChildSource = createFakeMediaSource(); - final FakeMediaSource unpreparedChildSource = - new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); + final FakeMediaSource unpreparedChildSource = new FakeMediaSource(/* timeline= */ null); dummyMainThread.runOnMainThread( () -> { mediaSource.addMediaSource(preparedChildSource); @@ -1092,13 +1091,13 @@ public final class ConcatenatingMediaSourceTest { private static FakeMediaSource[] createMediaSources(int count) { FakeMediaSource[] sources = new FakeMediaSource[count]; for (int i = 0; i < count; i++) { - sources[i] = new FakeMediaSource(createFakeTimeline(i), null); + sources[i] = new FakeMediaSource(createFakeTimeline(i)); } return sources; } private static FakeMediaSource createFakeMediaSource() { - return new FakeMediaSource(createFakeTimeline(/* index */ 0), null); + return new FakeMediaSource(createFakeTimeline(/* index */ 0)); } private static FakeTimeline createFakeTimeline(int index) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index df6506ed52..fa7c2f0614 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -135,7 +135,7 @@ public class LoopingMediaSourceTest { * Wraps the specified timeline in a {@link LoopingMediaSource} and returns the looping timeline. */ private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) throws IOException { - FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline); LoopingMediaSource mediaSource = new LoopingMediaSource(fakeMediaSource, loopCount); MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); try { @@ -153,7 +153,7 @@ public class LoopingMediaSourceTest { * the looping timeline can be created and prepared. */ private static void testMediaPeriodCreation(Timeline timeline, int loopCount) throws Exception { - FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline); LoopingMediaSource mediaSource = new LoopingMediaSource(fakeMediaSource, loopCount); MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); try { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java index 5ea15ac2e8..1434d28500 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java @@ -68,8 +68,7 @@ public class MergingMediaSourceTest { public void testMergingMediaSourcePeriodCreation() throws Exception { FakeMediaSource[] mediaSources = new FakeMediaSource[2]; for (int i = 0; i < mediaSources.length; i++) { - mediaSources[i] = - new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2), /* manifest= */ null); + mediaSources[i] = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2)); } MergingMediaSource mediaSource = new MergingMediaSource(mediaSources); MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); @@ -92,7 +91,7 @@ public class MergingMediaSourceTest { private static void testMergingMediaSourcePrepare(Timeline... timelines) throws IOException { FakeMediaSource[] mediaSources = new FakeMediaSource[timelines.length]; for (int i = 0; i < timelines.length; i++) { - mediaSources[i] = new FakeMediaSource(timelines[i], null); + mediaSources[i] = new FakeMediaSource(timelines[i]); } MergingMediaSource mergingMediaSource = new MergingMediaSource(mediaSources); MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mergingMediaSource, null); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java index bdd6820efa..701ec3521c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java @@ -62,6 +62,7 @@ public final class SinglePeriodTimelineTest { /* windowDefaultStartPositionUs= */ 0, /* isSeekable= */ false, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); // Should return null with a positive position projection beyond window duration. Pair position = @@ -84,6 +85,7 @@ public final class SinglePeriodTimelineTest { /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ false, + /* manifest= */ null, /* tag= */ null); assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ false).tag).isNull(); @@ -100,7 +102,11 @@ public final class SinglePeriodTimelineTest { Object tag = new Object(); SinglePeriodTimeline timeline = new SinglePeriodTimeline( - /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ false, tag); + /* durationUs= */ C.TIME_UNSET, + /* isSeekable= */ false, + /* isDynamic= */ false, + /* manifest= */ null, + tag); assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ false).tag).isNull(); assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ true).tag) @@ -114,6 +120,7 @@ public final class SinglePeriodTimelineTest { /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ false, + /* manifest= */ null, /* tag= */ null); Object uid = timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).uid; 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 551555502f..b9cb901041 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 @@ -994,7 +994,7 @@ public final class DashMediaSource extends BaseMediaSource { windowDefaultStartPositionUs, manifest, tag); - refreshSourceInfo(timeline, manifest); + refreshSourceInfo(timeline); if (!sideloadedManifest) { // Remove any pending simulated refresh. @@ -1193,6 +1193,7 @@ public final class DashMediaSource extends BaseMediaSource { && manifest.durationMs == C.TIME_UNSET; return window.set( tag, + manifest, presentationStartTimeMs, windowStartTimeMs, /* isSeekable= */ true, 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 f891670e78..12c6a8ee72 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 @@ -383,6 +383,7 @@ public final class HlsMediaSource extends BaseMediaSource ? windowStartTimeMs : C.TIME_UNSET; long windowDefaultStartPositionUs = playlist.startOffsetUs; + HlsManifest manifest = new HlsManifest(playlistTracker.getMasterPlaylist(), playlist); if (playlistTracker.isLive()) { long offsetFromInitialStartTimeUs = playlist.startTimeUs - playlistTracker.getInitialStartTimeUs(); @@ -403,6 +404,7 @@ public final class HlsMediaSource extends BaseMediaSource windowDefaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ !playlist.hasEndTag, + manifest, tag); } else /* not live */ { if (windowDefaultStartPositionUs == C.TIME_UNSET) { @@ -418,9 +420,10 @@ public final class HlsMediaSource extends BaseMediaSource windowDefaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ false, + manifest, tag); } - refreshSourceInfo(timeline, new HlsManifest(playlistTracker.getMasterPlaylist(), playlist)); + refreshSourceInfo(timeline); } } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index e31fbccae5..c053f255fc 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -669,6 +669,7 @@ public final class SsMediaSource extends BaseMediaSource /* windowDefaultStartPositionUs= */ 0, /* isSeekable= */ true, manifest.isLive, + manifest, tag); } else if (manifest.isLive) { if (manifest.dvrWindowLengthUs != C.TIME_UNSET && manifest.dvrWindowLengthUs > 0) { @@ -690,6 +691,7 @@ public final class SsMediaSource extends BaseMediaSource defaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ true, + manifest, tag); } else { long durationUs = manifest.durationUs != C.TIME_UNSET ? manifest.durationUs @@ -702,9 +704,10 @@ public final class SsMediaSource extends BaseMediaSource /* windowDefaultStartPositionUs= */ 0, /* isSeekable= */ true, /* isDynamic= */ false, + manifest, tag); } - refreshSourceInfo(timeline, manifest); + refreshSourceInfo(timeline); } private void scheduleManifestRefresh() { 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 bba422e488..e408035e98 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 @@ -1212,8 +1212,7 @@ public class PlayerControlView extends FrameLayout { } @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { + public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { updateNavigation(); updateTimeline(); } 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 c800c7bd63..9ad951cb17 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 @@ -1286,7 +1286,7 @@ public class PlayerNotificationManager { } @Override - public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) { + public void onTimelineChanged(Timeline timeline, int reason) { startOrUpdateNotification(); } 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 93e52bc23a..5d07f986d2 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 @@ -542,9 +542,7 @@ public abstract class Action { } } - /** - * Waits for {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)}. - */ + /** Waits for {@link Player.EventListener#onTimelineChanged(Timeline, int)}. */ public static final class WaitForTimelineChanged extends Action { @Nullable private final Timeline expectedTimeline; @@ -575,9 +573,7 @@ public abstract class Action { new Player.EventListener() { @Override public void onTimelineChanged( - Timeline timeline, - @Nullable Object manifest, - @Player.TimelineChangeReason int reason) { + Timeline timeline, @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 cc4b3a60d7..f7c6694409 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 @@ -309,9 +309,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc } if (mediaSource == null) { if (timeline == null) { - timeline = new FakeTimeline(1); + timeline = new FakeTimeline(/* windowCount= */ 1, manifest); } - mediaSource = new FakeMediaSource(timeline, manifest, supportedFormats); + mediaSource = new FakeMediaSource(timeline, supportedFormats); } if (expectedPlayerEndedCount == null) { expectedPlayerEndedCount = 1; @@ -347,7 +347,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private final CountDownLatch endedCountDownLatch; private final CountDownLatch actionScheduleFinishedCountDownLatch; private final ArrayList timelines; - private final ArrayList manifests; private final ArrayList timelineChangeReasons; private final ArrayList periodIndices; private final ArrayList discontinuityReasons; @@ -380,7 +379,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc this.eventListener = eventListener; this.analyticsListener = analyticsListener; this.timelines = new ArrayList<>(); - this.manifests = new ArrayList<>(); this.timelineChangeReasons = new ArrayList<>(); this.periodIndices = new ArrayList<>(); this.discontinuityReasons = new ArrayList<>(); @@ -469,9 +467,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc // Assertions called on the test thread after test finished. /** - * Asserts that the timelines reported by - * {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided - * timelines. + * Asserts that the timelines reported by {@link Player.EventListener#onTimelineChanged(Timeline, + * int)} are equal to the provided timelines. * * @param timelines A list of expected {@link Timeline}s. */ @@ -479,21 +476,10 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc assertThat(this.timelines).containsExactlyElementsIn(Arrays.asList(timelines)).inOrder(); } - /** - * Asserts that the manifests reported by - * {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided - * manifest. - * - * @param manifests A list of expected manifests. - */ - public void assertManifestsEqual(Object... manifests) { - assertThat(this.manifests).containsExactlyElementsIn(Arrays.asList(manifests)).inOrder(); - } - /** * Asserts that the timeline change reasons reported by {@link - * Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided - * timeline change reasons. + * Player.EventListener#onTimelineChanged(Timeline, int)} are equal to the provided timeline + * change reasons. */ public void assertTimelineChangeReasonsEqual(Integer... reasons) { assertThat(timelineChangeReasons).containsExactlyElementsIn(Arrays.asList(reasons)).inOrder(); @@ -573,10 +559,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc // Player.EventListener @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { + public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { timelines.add(timeline); - manifests.add(manifest); timelineChangeReasons.add(reason); if (reason == Player.TIMELINE_CHANGE_REASON_PREPARED) { periodIndices.add(player.getCurrentPeriodIndex()); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java index 5a158a3659..0d97b7a20f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java @@ -34,10 +34,9 @@ public class FakeAdaptiveMediaSource extends FakeMediaSource { public FakeAdaptiveMediaSource( Timeline timeline, - Object manifest, TrackGroupArray trackGroupArray, FakeChunkSource.Factory chunkSourceFactory) { - super(timeline, manifest, trackGroupArray); + super(timeline, trackGroupArray); this.chunkSourceFactory = chunkSourceFactory; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index 80456169ff..8e5ba230ac 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -56,7 +56,6 @@ public class FakeMediaSource extends BaseMediaSource { private final ArrayList createdMediaPeriods; protected Timeline timeline; - private Object manifest; private boolean preparedSource; private boolean releasedSource; private Handler sourceInfoRefreshHandler; @@ -68,8 +67,8 @@ public class FakeMediaSource extends BaseMediaSource { * null to prevent an immediate source info refresh message when preparing the media source. It * can be manually set later using {@link #setNewSourceInfo(Timeline, Object)}. */ - public FakeMediaSource(@Nullable Timeline timeline, Object manifest, Format... formats) { - this(timeline, manifest, buildTrackGroupArray(formats)); + public FakeMediaSource(@Nullable Timeline timeline, Format... formats) { + this(timeline, buildTrackGroupArray(formats)); } /** @@ -78,10 +77,8 @@ public class FakeMediaSource extends BaseMediaSource { * immediate source info refresh message when preparing the media source. It can be manually set * later using {@link #setNewSourceInfo(Timeline, Object)}. */ - public FakeMediaSource(@Nullable Timeline timeline, Object manifest, - TrackGroupArray trackGroupArray) { + public FakeMediaSource(@Nullable Timeline timeline, TrackGroupArray trackGroupArray) { this.timeline = timeline; - this.manifest = manifest; this.activeMediaPeriods = new ArrayList<>(); this.createdMediaPeriods = new ArrayList<>(); this.trackGroupArray = trackGroupArray; @@ -158,12 +155,10 @@ public class FakeMediaSource extends BaseMediaSource { assertThat(releasedSource).isFalse(); assertThat(preparedSource).isTrue(); timeline = newTimeline; - manifest = newManifest; finishSourcePreparation(); }); } else { timeline = newTimeline; - manifest = newManifest; } } @@ -212,7 +207,7 @@ public class FakeMediaSource extends BaseMediaSource { } private void finishSourcePreparation() { - refreshSourceInfo(timeline, manifest); + refreshSourceInfo(timeline); if (!timeline.isEmpty()) { MediaLoadData mediaLoadData = new MediaLoadData( diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 56438a51ef..58ee32cdd9 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -112,6 +112,7 @@ public final class FakeTimeline extends Timeline { private static final long AD_DURATION_US = 10 * C.MICROS_PER_SECOND; private final TimelineWindowDefinition[] windowDefinitions; + private final Object[] manifests; private final int[] periodOffsets; /** @@ -140,9 +141,10 @@ public final class FakeTimeline extends Timeline { * with a duration of {@link TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US} each. * * @param windowCount The number of windows. + * @param manifests The manifests of the windows. */ - public FakeTimeline(int windowCount) { - this(createDefaultWindowDefinitions(windowCount)); + public FakeTimeline(int windowCount, Object... manifests) { + this(manifests, createDefaultWindowDefinitions(windowCount)); } /** @@ -151,6 +153,18 @@ public final class FakeTimeline extends Timeline { * @param windowDefinitions A list of {@link TimelineWindowDefinition}s. */ public FakeTimeline(TimelineWindowDefinition... windowDefinitions) { + this(new Object[0], windowDefinitions); + } + + /** + * Creates a fake timeline with the given window definitions. + * + * @param windowDefinitions A list of {@link TimelineWindowDefinition}s. + */ + public FakeTimeline(Object[] manifests, TimelineWindowDefinition... windowDefinitions) { + this.manifests = new Object[windowDefinitions.length]; + System.arraycopy( + manifests, 0, this.manifests, 0, Math.min(this.manifests.length, manifests.length)); this.windowDefinitions = windowDefinitions; periodOffsets = new int[windowDefinitions.length + 1]; periodOffsets[0] = 0; @@ -171,6 +185,7 @@ public final class FakeTimeline extends Timeline { Object tag = setTag ? windowDefinition.id : null; return window.set( tag, + manifests[windowIndex], /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, windowDefinition.isSeekable, diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 0873dbd145..6d626088fc 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -345,7 +345,7 @@ public class MediaSourceTestRunner { // SourceInfoRefreshListener methods. @Override - public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) { + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); timelines.addLast(timeline); } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index df96b634dd..eaebe5a12d 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -195,11 +195,6 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } - @Override - public Object getCurrentManifest() { - throw new UnsupportedOperationException(); - } - @Override public Timeline getCurrentTimeline() { throw new UnsupportedOperationException(); From d4e3e8f2e085d0bc74a09b0bc5373af61212da06 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 10 Jul 2019 17:38:03 +0100 Subject: [PATCH 1435/1556] Add overridingDrmInitData to DecryptableSampleQueueReader To use in HLS when session keys are provided PiperOrigin-RevId: 257421156 --- .../source/DecryptableSampleQueueReader.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java index 4f0c5b87aa..42453929c4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java @@ -27,6 +27,8 @@ import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** @@ -39,6 +41,7 @@ public final class DecryptableSampleQueueReader { private final DrmSessionManager sessionManager; private final FormatHolder formatHolder; private final boolean playClearSamplesWithoutKeys; + private final HashMap overridingDrmInitDatas; private @MonotonicNonNull Format currentFormat; @Nullable private DrmSession currentSession; @@ -55,6 +58,19 @@ public final class DecryptableSampleQueueReader { formatHolder = new FormatHolder(); playClearSamplesWithoutKeys = (sessionManager.getFlags() & DrmSessionManager.FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS) != 0; + overridingDrmInitDatas = new HashMap<>(); + } + + /** + * Given a mapping from {@link DrmInitData#schemeType} to {@link DrmInitData}, overrides any + * {@link DrmInitData} read from the upstream {@link SampleQueue} whose {@link + * DrmInitData#schemeType} is a key in the mapping to use the corresponding {@link DrmInitData} + * value. If {@code overridingDrmInitDatas} does not contain a mapping for the upstream {@link + * DrmInitData#schemeType}, the upstream {@link DrmInitData} is used. + */ + public void setOverridingDrmInitDatas(Map overridingDrmInitDatas) { + this.overridingDrmInitDatas.clear(); + this.overridingDrmInitDatas.putAll(overridingDrmInitDatas); } /** Releases any resources acquired by this reader. */ @@ -170,6 +186,10 @@ public final class DecryptableSampleQueueReader { DrmSession previousSession = currentSession; DrmInitData drmInitData = currentFormat.drmInitData; if (drmInitData != null) { + DrmInitData overridingDrmInitData = overridingDrmInitDatas.get(drmInitData.schemeType); + if (overridingDrmInitData != null) { + drmInitData = overridingDrmInitData; + } currentSession = sessionManager.acquireSession(Assertions.checkNotNull(Looper.myLooper()), drmInitData); } else { From 6606a4ff010fcade2bd73c0dfad902075fd7f25c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 10 Jul 2019 20:21:42 +0100 Subject: [PATCH 1436/1556] CronetDataSource: Fix invalid Javadoc tag PiperOrigin-RevId: 257456890 --- .../google/android/exoplayer2/ext/cronet/CronetDataSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index b1f907cc37..ed92523017 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -554,7 +554,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if no data is available * because the end of the opened range has been reached. * @throws HttpDataSourceException If an error occurs reading from the source. - * @throws IllegalArgumentException If {@codes buffer} is not a direct ByteBuffer. + * @throws IllegalArgumentException If {@code buffer} is not a direct ByteBuffer. */ public int read(ByteBuffer buffer) throws HttpDataSourceException { Assertions.checkState(opened); From 972c6c2f5c5b164dea996c4dae6ed5d5308fd163 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 10 Jul 2019 20:34:33 +0100 Subject: [PATCH 1437/1556] Avoid acquiring DrmSessions using the dummy DrmSessionManager This is a temporary workaround until we have migrated all MediaSources uses. This change avoids having to migrate all uses of MediaSources immediately. PiperOrigin-RevId: 257459138 --- .../source/DecryptableSampleQueueReader.java | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java index 42453929c4..e33afaee60 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java @@ -131,7 +131,8 @@ public final class DecryptableSampleQueueReader { if (currentFormat == null || formatRequired) { readFlagFormatRequired = true; - } else if (currentFormat.drmInitData != null + } else if (sessionManager != DrmSessionManager.DUMMY + && currentFormat.drmInitData != null && Assertions.checkNotNull(currentSession).getState() != DrmSession.STATE_OPENED_WITH_KEYS) { if (playClearSamplesWithoutKeys) { @@ -158,12 +159,7 @@ public final class DecryptableSampleQueueReader { if (onlyPropagateFormatChanges && currentFormat == formatHolder.format) { return C.RESULT_NOTHING_READ; } - onFormat(Assertions.checkNotNull(formatHolder.format)); - // TODO: Remove once all Renderers and MediaSources have migrated to the new DRM model - // [Internal ref: b/129764794]. - outputFormatHolder.includesDrmSession = true; - outputFormatHolder.format = formatHolder.format; - outputFormatHolder.drmSession = currentSession; + onFormat(Assertions.checkNotNull(formatHolder.format), outputFormatHolder); } return result; } @@ -172,10 +168,21 @@ public final class DecryptableSampleQueueReader { * Updates the current format and manages any necessary DRM resources. * * @param format The format read from upstream. + * @param outputFormatHolder The output {@link FormatHolder}. */ - private void onFormat(Format format) { - DrmInitData oldDrmInitData = currentFormat != null ? currentFormat.drmInitData : null; + private void onFormat(Format format, FormatHolder outputFormatHolder) { + outputFormatHolder.format = format; currentFormat = format; + if (sessionManager == DrmSessionManager.DUMMY) { + // Avoid attempting to acquire a session using the dummy DRM session manager. It's likely that + // the media source creation has not yet been migrated and the renderer can acquire the + // session for the read DRM init data. + // TODO: Remove once renderers are migrated [Internal ref: b/122519809]. + return; + } + outputFormatHolder.includesDrmSession = true; + outputFormatHolder.drmSession = currentSession; + DrmInitData oldDrmInitData = currentFormat != null ? currentFormat.drmInitData : null; if (Util.areEqual(oldDrmInitData, format.drmInitData)) { // Nothing to do. return; @@ -195,6 +202,7 @@ public final class DecryptableSampleQueueReader { } else { currentSession = null; } + outputFormatHolder.drmSession = currentSession; if (previousSession != null) { previousSession.releaseReference(); @@ -211,8 +219,9 @@ public final class DecryptableSampleQueueReader { } else if (nextInQueue == SampleQueue.PEEK_RESULT_BUFFER_CLEAR) { return currentSession == null || playClearSamplesWithoutKeys; } else if (nextInQueue == SampleQueue.PEEK_RESULT_BUFFER_ENCRYPTED) { - return Assertions.checkNotNull(currentSession).getState() - == DrmSession.STATE_OPENED_WITH_KEYS; + return sessionManager == DrmSessionManager.DUMMY + || Assertions.checkNotNull(currentSession).getState() + == DrmSession.STATE_OPENED_WITH_KEYS; } else { throw new IllegalStateException(); } From 6796b179a6a0be7e85a9237bed7806e6d059b489 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 10 Jul 2019 22:11:55 +0100 Subject: [PATCH 1438/1556] Make onInputFormatChanged methods in Renderers take FormatHolders PiperOrigin-RevId: 257478434 --- .../exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 14 +++++++------- .../audio/SimpleDecoderAudioRenderer.java | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index b4db4971cc..56f5fd2d09 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -307,7 +307,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { flagsOnlyBuffer.clear(); int result = readSource(formatHolder, flagsOnlyBuffer, true); if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); + onInputFormatChanged(formatHolder); } else if (result == C.RESULT_BUFFER_READ) { // End of stream read having not read a format. Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); @@ -491,15 +491,15 @@ public class LibvpxVideoRenderer extends BaseRenderer { /** * Called when a new format is read from the upstream source. * - * @param newFormat The new format. + * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}. * @throws ExoPlaybackException If an error occurs (re-)initializing the decoder. */ @CallSuper @SuppressWarnings("unchecked") - protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { + protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { Format oldFormat = format; - format = newFormat; - pendingFormat = newFormat; + format = formatHolder.format; + pendingFormat = format; boolean drmInitDataChanged = !Util.areEqual(format.drmInitData, oldFormat == null ? null : oldFormat.drmInitData); @@ -513,7 +513,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); } DrmSession session = - drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); + drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData); if (sourceDrmSession != null) { sourceDrmSession.releaseReference(); } @@ -826,7 +826,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { return false; } if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); + onInputFormatChanged(formatHolder); return true; } if (inputBuffer.isEndOfStream()) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 93c60f2917..ef0207517a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -274,7 +274,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements flagsOnlyBuffer.clear(); int result = readSource(formatHolder, flagsOnlyBuffer, true); if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); + onInputFormatChanged(formatHolder); } else if (result == C.RESULT_BUFFER_READ) { // End of stream read having not read a format. Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); @@ -438,7 +438,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements return false; } if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); + onInputFormatChanged(formatHolder); return true; } if (inputBuffer.isEndOfStream()) { @@ -656,9 +656,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } @SuppressWarnings("unchecked") - private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { + private void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { Format oldFormat = inputFormat; - inputFormat = newFormat; + inputFormat = formatHolder.format; boolean drmInitDataChanged = !Util.areEqual(inputFormat.drmInitData, oldFormat == null ? null : oldFormat.drmInitData); @@ -673,7 +673,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); } DrmSession session = - drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); + drmSessionManager.acquireSession(Looper.myLooper(), inputFormat.drmInitData); if (sourceDrmSession != null) { sourceDrmSession.releaseReference(); } @@ -694,10 +694,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements audioTrackNeedsConfigure = true; } - encoderDelay = newFormat.encoderDelay; - encoderPadding = newFormat.encoderPadding; + encoderDelay = inputFormat.encoderDelay; + encoderPadding = inputFormat.encoderPadding; - eventDispatcher.inputFormatChanged(newFormat); + eventDispatcher.inputFormatChanged(inputFormat); } private void onQueueInputBuffer(DecoderInputBuffer buffer) { From 91750b80098a0c721bbc45158912737517f80c69 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 11 Jul 2019 11:16:54 +0100 Subject: [PATCH 1439/1556] Plumb DrmSessionManager into DashMediaSource PiperOrigin-RevId: 257576791 --- .../exoplayer2/demo/PlayerActivity.java | 21 +++++++++++---- .../source/DecryptableSampleQueueReader.java | 3 +-- .../source/chunk/ChunkSampleStream.java | 23 ++++++++++------ .../source/dash/DashMediaPeriod.java | 25 +++++++++++++++--- .../source/dash/DashMediaSource.java | 26 +++++++++++++++++++ .../source/dash/DashMediaPeriodTest.java | 2 ++ .../dash/offline/DownloadHelperTest.java | 3 ++- .../source/smoothstreaming/SsMediaPeriod.java | 2 ++ .../testutil/FakeAdaptiveMediaPeriod.java | 2 ++ 9 files changed, 87 insertions(+), 20 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 929b579b4c..59d861e13d 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 @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; @@ -358,7 +359,7 @@ public class PlayerActivity extends AppCompatActivity return; } - DefaultDrmSessionManager drmSessionManager = null; + DrmSessionManager drmSessionManager = null; if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) { String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA); String[] keyRequestPropertiesArray = @@ -389,6 +390,8 @@ public class PlayerActivity extends AppCompatActivity finish(); return; } + } else { + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); } TrackSelection.Factory trackSelectionFactory; @@ -425,7 +428,7 @@ public class PlayerActivity extends AppCompatActivity MediaSource[] mediaSources = new MediaSource[uris.length]; for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i]); + mediaSources[i] = buildMediaSource(uris[i], extensions[i], drmSessionManager); } mediaSource = mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); @@ -455,10 +458,16 @@ public class PlayerActivity extends AppCompatActivity } private MediaSource buildMediaSource(Uri uri) { - return buildMediaSource(uri, null); + return buildMediaSource( + uri, + /* overrideExtension= */ null, + /* drmSessionManager= */ DrmSessionManager.getDummyDrmSessionManager()); } - private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { + private MediaSource buildMediaSource( + Uri uri, + @Nullable String overrideExtension, + DrmSessionManager drmSessionManager) { DownloadRequest downloadRequest = ((DemoApplication) getApplication()).getDownloadTracker().getDownloadRequest(uri); if (downloadRequest != null) { @@ -467,7 +476,9 @@ public class PlayerActivity extends AppCompatActivity @ContentType int type = Util.inferContentType(uri, overrideExtension); switch (type) { case C.TYPE_DASH: - return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + return new DashMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); case C.TYPE_SS: return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_HLS: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java index e33afaee60..b0b10d4e98 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java @@ -172,6 +172,7 @@ public final class DecryptableSampleQueueReader { */ private void onFormat(Format format, FormatHolder outputFormatHolder) { outputFormatHolder.format = format; + DrmInitData oldDrmInitData = currentFormat != null ? currentFormat.drmInitData : null; currentFormat = format; if (sessionManager == DrmSessionManager.DUMMY) { // Avoid attempting to acquire a session using the dummy DRM session manager. It's likely that @@ -182,12 +183,10 @@ public final class DecryptableSampleQueueReader { } outputFormatHolder.includesDrmSession = true; outputFormatHolder.drmSession = currentSession; - DrmInitData oldDrmInitData = currentFormat != null ? currentFormat.drmInitData : null; if (Util.areEqual(oldDrmInitData, format.drmInitData)) { // Nothing to do. return; } - // Ensure we acquire the new session before releasing the previous one in case the same session // can be used for both DrmInitData. DrmSession previousSession = currentSession; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index efc3b47596..6eaeefec6b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -21,6 +21,9 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.source.DecryptableSampleQueueReader; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleStream; @@ -71,6 +74,7 @@ public class ChunkSampleStream implements SampleStream, S private final ArrayList mediaChunks; private final List readOnlyMediaChunks; private final SampleQueue primarySampleQueue; + private final DecryptableSampleQueueReader primarySampleQueueReader; private final SampleQueue[] embeddedSampleQueues; private final BaseMediaChunkOutput mediaChunkOutput; @@ -94,6 +98,8 @@ public class ChunkSampleStream implements SampleStream, S * @param callback An {@link Callback} for the stream. * @param allocator An {@link Allocator} from which allocations can be obtained. * @param positionUs The position from which to start loading media. + * @param drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions} + * from. * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. * @param eventDispatcher A dispatcher to notify of events. */ @@ -105,6 +111,7 @@ public class ChunkSampleStream implements SampleStream, S Callback> callback, Allocator allocator, long positionUs, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher) { this.primaryTrackType = primaryTrackType; @@ -126,6 +133,8 @@ public class ChunkSampleStream implements SampleStream, S SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount]; primarySampleQueue = new SampleQueue(allocator); + primarySampleQueueReader = + new DecryptableSampleQueueReader(primarySampleQueue, drmSessionManager); trackTypes[0] = primaryTrackType; sampleQueues[0] = primarySampleQueue; @@ -328,6 +337,7 @@ public class ChunkSampleStream implements SampleStream, S this.releaseCallback = callback; // Discard as much as we can synchronously. primarySampleQueue.discardToEnd(); + primarySampleQueueReader.release(); for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { embeddedSampleQueue.discardToEnd(); } @@ -349,12 +359,13 @@ public class ChunkSampleStream implements SampleStream, S @Override public boolean isReady() { - return loadingFinished || (!isPendingReset() && primarySampleQueue.hasNextSample()); + return !isPendingReset() && primarySampleQueueReader.isReady(loadingFinished); } @Override public void maybeThrowError() throws IOException { loader.maybeThrowError(); + primarySampleQueueReader.maybeThrowError(); if (!loader.isLoading()) { chunkSource.maybeThrowError(); } @@ -367,13 +378,9 @@ public class ChunkSampleStream implements SampleStream, S return C.RESULT_NOTHING_READ; } maybeNotifyPrimaryTrackFormatChanged(); - return primarySampleQueue.read( - formatHolder, - buffer, - formatRequired, - /* allowOnlyClearBuffers= */ false, - loadingFinished, - decodeOnlyUntilPositionUs); + + return primarySampleQueueReader.read( + formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); } @Override 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 be0aa4f154..b34b677d45 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 @@ -22,6 +22,8 @@ import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.EmptySampleStream; @@ -70,6 +72,7 @@ import java.util.regex.Pattern; /* package */ final int id; private final DashChunkSource.Factory chunkSourceFactory; @Nullable private final TransferListener transferListener; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long elapsedRealtimeOffsetMs; private final LoaderErrorThrower manifestLoaderErrorThrower; @@ -97,6 +100,7 @@ import java.util.regex.Pattern; int periodIndex, DashChunkSource.Factory chunkSourceFactory, @Nullable TransferListener transferListener, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, long elapsedRealtimeOffsetMs, @@ -109,6 +113,7 @@ import java.util.regex.Pattern; this.periodIndex = periodIndex; this.chunkSourceFactory = chunkSourceFactory; this.transferListener = transferListener; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs; @@ -123,8 +128,8 @@ import java.util.regex.Pattern; compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); Period period = manifest.getPeriod(periodIndex); eventStreams = period.eventStreams; - Pair result = buildTrackGroups(period.adaptationSets, - eventStreams); + Pair result = + buildTrackGroups(drmSessionManager, period.adaptationSets, eventStreams); trackGroups = result.first; trackGroupInfos = result.second; eventDispatcher.mediaPeriodCreated(); @@ -455,7 +460,9 @@ import java.util.regex.Pattern; } private static Pair buildTrackGroups( - List adaptationSets, List eventStreams) { + DrmSessionManager drmSessionManager, + List adaptationSets, + List eventStreams) { int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets); int primaryGroupCount = groupedAdaptationSetIndices.length; @@ -475,6 +482,7 @@ import java.util.regex.Pattern; int trackGroupCount = buildPrimaryAndEmbeddedTrackGroupInfos( + drmSessionManager, adaptationSets, groupedAdaptationSetIndices, primaryGroupCount, @@ -569,6 +577,7 @@ import java.util.regex.Pattern; } private static int buildPrimaryAndEmbeddedTrackGroupInfos( + DrmSessionManager drmSessionManager, List adaptationSets, int[][] groupedAdaptationSetIndices, int primaryGroupCount, @@ -585,7 +594,14 @@ import java.util.regex.Pattern; } Format[] formats = new Format[representations.size()]; for (int j = 0; j < formats.length; j++) { - formats[j] = representations.get(j).format; + Format format = representations.get(j).format; + DrmInitData drmInitData = format.drmInitData; + if (drmInitData != null) { + format = + format.copyWithExoMediaCryptoType( + drmSessionManager.getExoMediaCryptoType(drmInitData)); + } + formats[j] = format; } AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]); @@ -692,6 +708,7 @@ import java.util.regex.Pattern; this, allocator, positionUs, + drmSessionManager, loadErrorHandlingPolicy, eventDispatcher); synchronized (this) { 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 b9cb901041..92c47f2f12 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 @@ -25,6 +25,8 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.FilteringManifestParser; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.BaseMediaSource; @@ -79,6 +81,7 @@ public final class DashMediaSource extends BaseMediaSource { private final DashChunkSource.Factory chunkSourceFactory; @Nullable private final DataSource.Factory manifestDataSourceFactory; + private DrmSessionManager drmSessionManager; @Nullable private ParsingLoadable.Parser manifestParser; @Nullable private List streamKeys; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; @@ -112,6 +115,7 @@ public final class DashMediaSource extends BaseMediaSource { @Nullable DataSource.Factory manifestDataSourceFactory) { this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); @@ -132,6 +136,20 @@ public final class DashMediaSource extends BaseMediaSource { return this; } + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManager = drmSessionManager; + return this; + } + /** * Sets the minimum number of times to retry if a loading error occurs. See {@link * #setLoadErrorHandlingPolicy} for the default value. @@ -253,6 +271,7 @@ public final class DashMediaSource extends BaseMediaSource { /* manifestParser= */ null, chunkSourceFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, livePresentationDelayOverridesManifest, @@ -313,6 +332,7 @@ public final class DashMediaSource extends BaseMediaSource { manifestParser, chunkSourceFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, livePresentationDelayOverridesManifest, @@ -361,6 +381,7 @@ public final class DashMediaSource extends BaseMediaSource { private final DataSource.Factory manifestDataSourceFactory; private final DashChunkSource.Factory chunkSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long livePresentationDelayMs; private final boolean livePresentationDelayOverridesManifest; @@ -443,6 +464,7 @@ public final class DashMediaSource extends BaseMediaSource { /* manifestParser= */ null, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), + DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), DEFAULT_LIVE_PRESENTATION_DELAY_MS, /* livePresentationDelayOverridesManifest= */ false, @@ -556,6 +578,7 @@ public final class DashMediaSource extends BaseMediaSource { manifestParser, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), + DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), livePresentationDelayMs == DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS ? DEFAULT_LIVE_PRESENTATION_DELAY_MS @@ -574,6 +597,7 @@ public final class DashMediaSource extends BaseMediaSource { ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, long livePresentationDelayMs, boolean livePresentationDelayOverridesManifest, @@ -584,6 +608,7 @@ public final class DashMediaSource extends BaseMediaSource { this.manifestDataSourceFactory = manifestDataSourceFactory; this.manifestParser = manifestParser; this.chunkSourceFactory = chunkSourceFactory; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.livePresentationDelayMs = livePresentationDelayMs; this.livePresentationDelayOverridesManifest = livePresentationDelayOverridesManifest; @@ -660,6 +685,7 @@ public final class DashMediaSource extends BaseMediaSource { periodIndex, chunkSourceFactory, mediaTransferListener, + drmSessionManager, loadErrorHandlingPolicy, periodEventDispatcher, elapsedRealtimeOffsetMs, diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java index fa077df209..f39a493e9f 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java @@ -22,6 +22,7 @@ import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -116,6 +117,7 @@ public final class DashMediaPeriodTest { periodIndex, mock(DashChunkSource.Factory.class), mock(TransferListener.class), + DrmSessionManager.getDummyDrmSessionManager(), mock(LoadErrorHandlingPolicy.class), new EventDispatcher() .withParameters( diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java index 0b7c06f813..73225f68c7 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.dash.offline; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.testutil.FakeDataSource; import org.junit.Test; @@ -37,7 +38,7 @@ public final class DownloadHelperTest { Uri.parse("http://uri"), new FakeDataSource.Factory(), (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0], - /* drmSessionManager= */ null, + /* drmSessionManager= */ DrmSessionManager.getDummyDrmSessionManager(), DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS); } } 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 38781782eb..6d4db3fbbb 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; @@ -237,6 +238,7 @@ import java.util.List; this, allocator, positionUs, + DrmSessionManager.getDummyDrmSessionManager(), loadErrorHandlingPolicy, eventDispatcher); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index fea863c48e..bcb97be287 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.testutil; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -149,6 +150,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod /* callback= */ this, allocator, /* positionUs= */ 0, + /* drmSessionManager= */ DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(/* minimumLoadableRetryCount= */ 3), eventDispatcher); } From 3f24d4433ae36004672567b8d22cfb0f88fa4c96 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 11 Jul 2019 11:33:38 +0100 Subject: [PATCH 1440/1556] Optimize DrmSession reference replacement Potentially avoids up to two calls to synchronized methods PiperOrigin-RevId: 257578304 --- .../java/com/google/android/exoplayer2/drm/DrmSession.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index df45323ca3..722ab946f0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -31,10 +31,15 @@ public interface DrmSession { /** * Invokes {@code newSession's} {@link #acquireReference()} and {@code previousSession's} {@link - * #releaseReference()} in that order. Does nothing for passed null values. + * #releaseReference()} in that order. Null arguments are ignored. Does nothing if {@code + * previousSession} and {@code newSession} are the same session. */ static void replaceSessionReferences( @Nullable DrmSession previousSession, @Nullable DrmSession newSession) { + if (previousSession == newSession) { + // Do nothing. + return; + } if (newSession != null) { newSession.acquireReference(); } From bbcd46e98aac55245f8ec90925dd20e0213f998f Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 11 Jul 2019 14:42:51 +0100 Subject: [PATCH 1441/1556] Change HlsSampleStreamWrapper.prepareWithMasterPlaylistInfo to take a TrackGroup[] Non-functional change. Makes it easier to add the ExoMediaCrypto type information to the formats. PiperOrigin-RevId: 257598282 --- .../exoplayer2/source/hls/HlsMediaPeriod.java | 11 +++---- .../source/hls/HlsSampleStreamWrapper.java | 30 +++++++++++-------- 2 files changed, 23 insertions(+), 18 deletions(-) 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 d834c097cf..39b49da402 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 @@ -487,7 +487,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper manifestUrlIndicesPerWrapper.add(new int[] {i}); sampleStreamWrappers.add(sampleStreamWrapper); sampleStreamWrapper.prepareWithMasterPlaylistInfo( - new TrackGroupArray(new TrackGroup(subtitleRendition.format)), 0, TrackGroupArray.EMPTY); + new TrackGroup[] {new TrackGroup(subtitleRendition.format)}, + /* primaryTrackGroupIndex= */ 0); } this.sampleStreamWrappers = sampleStreamWrappers.toArray(new HlsSampleStreamWrapper[0]); @@ -645,9 +646,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper muxedTrackGroups.add(id3TrackGroup); sampleStreamWrapper.prepareWithMasterPlaylistInfo( - new TrackGroupArray(muxedTrackGroups.toArray(new TrackGroup[0])), - 0, - new TrackGroupArray(id3TrackGroup)); + muxedTrackGroups.toArray(new TrackGroup[0]), + /* primaryTrackGroupIndex= */ 0, + /* optionalTrackGroupsIndices= */ muxedTrackGroups.indexOf(id3TrackGroup)); } } @@ -703,7 +704,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper if (allowChunklessPreparation && renditionsHaveCodecs) { Format[] renditionFormats = scratchPlaylistFormats.toArray(new Format[0]); sampleStreamWrapper.prepareWithMasterPlaylistInfo( - new TrackGroupArray(new TrackGroup(renditionFormats)), 0, TrackGroupArray.EMPTY); + new TrackGroup[] {new TrackGroup(renditionFormats)}, /* primaryTrackGroupIndex= */ 0); } } } 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 96704053cb..079852c4d4 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 @@ -51,8 +51,10 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides @@ -122,7 +124,7 @@ import java.util.Map; // Tracks are complicated in HLS. See documentation of buildTracks for details. // Indexed by track (as exposed by this source). private TrackGroupArray trackGroups; - private TrackGroupArray optionalTrackGroups; + private Set optionalTrackGroups; // Indexed by track group. private int[] trackGroupToSampleQueueIndex; private int primaryTrackGroupIndex; @@ -200,18 +202,20 @@ import java.util.Map; /** * Prepares the sample stream wrapper with master playlist information. * - * @param trackGroups The {@link TrackGroupArray} to expose. + * @param trackGroups The {@link TrackGroup TrackGroups} to expose through {@link + * #getTrackGroups()}. * @param primaryTrackGroupIndex The index of the adaptive track group. - * @param optionalTrackGroups A subset of {@code trackGroups} that should not trigger a failure if - * not found in the media playlist's segments. + * @param optionalTrackGroupsIndices The indices of any {@code trackGroups} that should not + * trigger a failure if not found in the media playlist's segments. */ public void prepareWithMasterPlaylistInfo( - TrackGroupArray trackGroups, - int primaryTrackGroupIndex, - TrackGroupArray optionalTrackGroups) { + TrackGroup[] trackGroups, int primaryTrackGroupIndex, int... optionalTrackGroupsIndices) { prepared = true; - this.trackGroups = trackGroups; - this.optionalTrackGroups = optionalTrackGroups; + this.trackGroups = new TrackGroupArray(trackGroups); + optionalTrackGroups = new HashSet<>(); + for (int optionalTrackGroupIndex : optionalTrackGroupsIndices) { + optionalTrackGroups.add(this.trackGroups.get(optionalTrackGroupIndex)); + } this.primaryTrackGroupIndex = primaryTrackGroupIndex; handler.post(callback::onPrepared); } @@ -231,9 +235,9 @@ import java.util.Map; public int bindSampleQueueToSampleStream(int trackGroupIndex) { int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; if (sampleQueueIndex == C.INDEX_UNSET) { - return optionalTrackGroups.indexOf(trackGroups.get(trackGroupIndex)) == C.INDEX_UNSET - ? SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL - : SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL; + return optionalTrackGroups.contains(trackGroups.get(trackGroupIndex)) + ? SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL + : SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL; } if (sampleQueuesEnabledStates[sampleQueueIndex]) { // This sample queue is already bound to a different sample stream. @@ -1046,7 +1050,7 @@ import java.util.Map; } this.trackGroups = new TrackGroupArray(trackGroups); Assertions.checkState(optionalTrackGroups == null); - optionalTrackGroups = TrackGroupArray.EMPTY; + optionalTrackGroups = Collections.emptySet(); } private HlsMediaChunk getLastMediaChunk() { From b9ab0cf137495eba377311c1b7dc572b78797a01 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 11 Jul 2019 17:38:31 +0100 Subject: [PATCH 1442/1556] Plumb DrmSessionManager into SsMediaSource PiperOrigin-RevId: 257624043 --- .../exoplayer2/demo/PlayerActivity.java | 4 ++- .../source/smoothstreaming/SsMediaPeriod.java | 23 +++++++++++++--- .../source/smoothstreaming/SsMediaSource.java | 26 +++++++++++++++++++ .../smoothstreaming/SsMediaPeriodTest.java | 2 ++ 4 files changed, 50 insertions(+), 5 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 59d861e13d..3ade9173c6 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 @@ -480,7 +480,9 @@ public class PlayerActivity extends AppCompatActivity .setDrmSessionManager(drmSessionManager) .createMediaSource(uri); case C.TYPE_SS: - return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + return new SsMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); case C.TYPE_HLS: return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_OTHER: 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 6d4db3fbbb..286ec82ed6 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.smoothstreaming; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.StreamKey; @@ -45,6 +46,7 @@ import java.util.List; private final SsChunkSource.Factory chunkSourceFactory; @Nullable private final TransferListener transferListener; private final LoaderErrorThrower manifestLoaderErrorThrower; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Allocator allocator; @@ -62,6 +64,7 @@ import java.util.List; SsChunkSource.Factory chunkSourceFactory, @Nullable TransferListener transferListener, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, LoaderErrorThrower manifestLoaderErrorThrower, @@ -70,11 +73,12 @@ import java.util.List; this.chunkSourceFactory = chunkSourceFactory; this.transferListener = transferListener; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.allocator = allocator; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; - trackGroups = buildTrackGroups(manifest); + trackGroups = buildTrackGroups(manifest, drmSessionManager); sampleStreams = newSampleStreamArray(0); compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); @@ -238,15 +242,26 @@ import java.util.List; this, allocator, positionUs, - DrmSessionManager.getDummyDrmSessionManager(), + drmSessionManager, loadErrorHandlingPolicy, eventDispatcher); } - private static TrackGroupArray buildTrackGroups(SsManifest manifest) { + private static TrackGroupArray buildTrackGroups( + SsManifest manifest, DrmSessionManager drmSessionManager) { TrackGroup[] trackGroups = new TrackGroup[manifest.streamElements.length]; for (int i = 0; i < manifest.streamElements.length; i++) { - trackGroups[i] = new TrackGroup(manifest.streamElements[i].formats); + Format[] manifestFormats = manifest.streamElements[i].formats; + Format[] exposedFormats = new Format[manifestFormats.length]; + for (int j = 0; j < manifestFormats.length; j++) { + Format manifestFormat = manifestFormats[j]; + exposedFormats[j] = + manifestFormat.drmInitData != null + ? manifestFormat.copyWithExoMediaCryptoType( + drmSessionManager.getExoMediaCryptoType(manifestFormat.drmInitData)) + : manifestFormat; + } + trackGroups[i] = new TrackGroup(exposedFormats); } return new TrackGroupArray(trackGroups); } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index c053f255fc..3c0593200e 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -22,6 +22,8 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.FilteringManifestParser; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.BaseMediaSource; @@ -69,6 +71,7 @@ public final class SsMediaSource extends BaseMediaSource @Nullable private ParsingLoadable.Parser manifestParser; @Nullable private List streamKeys; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private DrmSessionManager drmSessionManager; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private long livePresentationDelayMs; private boolean isCreateCalled; @@ -98,6 +101,7 @@ public final class SsMediaSource extends BaseMediaSource @Nullable DataSource.Factory manifestDataSourceFactory) { this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); @@ -117,6 +121,20 @@ public final class SsMediaSource extends BaseMediaSource return this; } + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManager = drmSessionManager; + return this; + } + /** * Sets the minimum number of times to retry if a loading error occurs. See {@link * #setLoadErrorHandlingPolicy} for the default value. @@ -220,6 +238,7 @@ public final class SsMediaSource extends BaseMediaSource /* manifestParser= */ null, chunkSourceFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, tag); @@ -279,6 +298,7 @@ public final class SsMediaSource extends BaseMediaSource manifestParser, chunkSourceFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, tag); @@ -318,6 +338,7 @@ public final class SsMediaSource extends BaseMediaSource private final DataSource.Factory manifestDataSourceFactory; private final SsChunkSource.Factory chunkSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long livePresentationDelayMs; private final EventDispatcher manifestEventDispatcher; @@ -383,6 +404,7 @@ public final class SsMediaSource extends BaseMediaSource /* manifestParser= */ null, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), + DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), DEFAULT_LIVE_PRESENTATION_DELAY_MS, /* tag= */ null); @@ -483,6 +505,7 @@ public final class SsMediaSource extends BaseMediaSource manifestParser, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), + DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), livePresentationDelayMs, /* tag= */ null); @@ -498,6 +521,7 @@ public final class SsMediaSource extends BaseMediaSource ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, long livePresentationDelayMs, @Nullable Object tag) { @@ -508,6 +532,7 @@ public final class SsMediaSource extends BaseMediaSource this.manifestParser = manifestParser; this.chunkSourceFactory = chunkSourceFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.livePresentationDelayMs = livePresentationDelayMs; this.manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); @@ -553,6 +578,7 @@ public final class SsMediaSource extends BaseMediaSource chunkSourceFactory, mediaTransferListener, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, eventDispatcher, manifestLoaderErrorThrower, diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java index 787659fffe..b9c63f843d 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.mock; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -66,6 +67,7 @@ public class SsMediaPeriodTest { mock(SsChunkSource.Factory.class), mock(TransferListener.class), mock(CompositeSequenceableLoaderFactory.class), + mock(DrmSessionManager.class), mock(LoadErrorHandlingPolicy.class), new EventDispatcher() .withParameters( From 31d20a9a973c3310990c87b60e392a538304fda0 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 11 Jul 2019 18:11:08 +0100 Subject: [PATCH 1443/1556] Add missing file header PiperOrigin-RevId: 257630168 --- publish.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/publish.gradle b/publish.gradle index f293673c49..8cfc2b2ea1 100644 --- a/publish.gradle +++ b/publish.gradle @@ -60,6 +60,7 @@ static void addLicense(File pom) { xml.append(licensesNode) def writer = new PrintWriter(new FileWriter(pom)) + writer.write("\n") def printer = new XmlNodePrinter(writer) printer.preserveWhitespace = true printer.print(xml) From ccc82cdb4acf174a77b120a2dbf898b7865bcb3d Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 12 Jul 2019 08:54:41 +0100 Subject: [PATCH 1444/1556] Remove some remaining references to manifests. PiperOrigin-RevId: 257757496 --- .../src/main/java/com/google/android/exoplayer2/Player.java | 4 ++-- .../com/google/android/exoplayer2/source/MediaSource.java | 6 +++--- .../android/exoplayer2/source/dash/DashMediaSource.java | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) 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 68a386d2de..4e062dcb5e 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 @@ -563,8 +563,8 @@ public interface Player { int DISCONTINUITY_REASON_INTERNAL = 4; /** - * Reasons for timeline and/or manifest changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED}, - * {@link #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}. + * Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED}, {@link + * #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}. */ @Documented @Retention(RetentionPolicy.SOURCE) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 10e29f3f44..bd87eb6509 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -239,12 +239,12 @@ public interface MediaSource { } /** - * Starts source preparation if not yet started, and adds a listener for timeline and/or manifest - * updates. + * Starts source preparation. * *

        Should not be called directly from application code. * - *

        The listener will be also be notified if the source already has a timeline and/or manifest. + *

        {@link SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline)} will be + * called once the source has a {@link Timeline}. * *

        For each call to this method, a call to {@link #releaseSource(SourceInfoRefreshListener)} is * needed to remove the listener and to release the source if no longer required. 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 92c47f2f12..f7387ee77c 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 @@ -366,8 +366,8 @@ public final class DashMediaSource extends BaseMediaSource { /** * The interval in milliseconds between invocations of {@link - * SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} when the - * source's {@link Timeline} is changing dynamically (for example, for incomplete live streams). + * SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline)} when the source's + * {@link Timeline} is changing dynamically (for example, for incomplete live streams). */ private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000; /** From df81bd6a682289ec7781cea2249353f6a71bceab Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 12 Jul 2019 08:55:08 +0100 Subject: [PATCH 1445/1556] Make ExtractorMediaSource a CompositeMediaSource instead of just wrapping. It's easy to forget to forward methods when using basic wrapping. For example, ExtractorMediaSource.addEventListener is currently a no-op because it's not forwarded. PiperOrigin-RevId: 257757556 --- .../source/ExtractorMediaSource.java | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 2bcaad4fce..7332ed74e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -35,8 +35,7 @@ import java.io.IOException; /** @deprecated Use {@link ProgressiveMediaSource} instead. */ @Deprecated @SuppressWarnings("deprecation") -public final class ExtractorMediaSource extends BaseMediaSource - implements MediaSource.SourceInfoRefreshListener { +public final class ExtractorMediaSource extends CompositeMediaSource { /** @deprecated Use {@link MediaSourceEventListener} instead. */ @Deprecated @@ -340,12 +339,14 @@ public final class ExtractorMediaSource extends BaseMediaSource @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { - progressiveMediaSource.prepareSource(/* listener= */ this, mediaTransferListener); + super.prepareSourceInternal(mediaTransferListener); + prepareChildSource(/* id= */ null, progressiveMediaSource); } @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - progressiveMediaSource.maybeThrowSourceInfoRefreshError(); + protected void onChildSourceInfoRefreshed( + @Nullable Void id, MediaSource mediaSource, Timeline timeline) { + refreshSourceInfo(timeline); } @Override @@ -358,16 +359,6 @@ public final class ExtractorMediaSource extends BaseMediaSource progressiveMediaSource.releasePeriod(mediaPeriod); } - @Override - protected void releaseSourceInternal() { - progressiveMediaSource.releaseSource(/* listener= */ this); - } - - @Override - public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { - refreshSourceInfo(timeline); - } - @Deprecated private static final class EventListenerWrapper extends DefaultMediaSourceEventListener { From 510f1883b055b042425555378cdf7302decf88b1 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 12 Jul 2019 12:01:18 +0100 Subject: [PATCH 1446/1556] Plumb DrmSessionManager into ProgressiveMediaSource PiperOrigin-RevId: 257777513 --- .../exoplayer2/demo/PlayerActivity.java | 4 +- .../source/ExtractorMediaSource.java | 2 + .../source/ProgressiveMediaPeriod.java | 42 +++++++++++++------ .../source/ProgressiveMediaSource.java | 23 ++++++++++ 4 files changed, 58 insertions(+), 13 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 3ade9173c6..249223bff5 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 @@ -486,7 +486,9 @@ public class PlayerActivity extends AppCompatActivity case C.TYPE_HLS: return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_OTHER: - return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + return new ProgressiveMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); default: throw new IllegalStateException("Unsupported type: " + type); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 7332ed74e0..9e7da87766 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -21,6 +21,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; @@ -325,6 +326,7 @@ public final class ExtractorMediaSource extends CompositeMediaSource { uri, dataSourceFactory, extractorsFactory, + DrmSessionManager.getDummyDrmSessionManager(), loadableLoadErrorHandlingPolicy, customCacheKey, continueLoadingCheckIntervalBytes, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index a56d14083e..83145d04b0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -90,6 +91,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private final Uri uri; private final DataSource dataSource; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Listener listener; @@ -107,6 +109,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Nullable private SeekMap seekMap; @Nullable private IcyHeaders icyHeaders; private SampleQueue[] sampleQueues; + private DecryptableSampleQueueReader[] sampleQueueReaders; private TrackId[] sampleQueueTrackIds; private boolean sampleQueuesBuilt; private boolean prepared; @@ -152,6 +155,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; Uri uri, DataSource dataSource, Extractor[] extractors, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, Listener listener, @@ -160,6 +164,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; int continueLoadingCheckIntervalBytes) { this.uri = uri; this.dataSource = dataSource; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.listener = listener; @@ -180,6 +185,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; handler = new Handler(); sampleQueueTrackIds = new TrackId[0]; sampleQueues = new SampleQueue[0]; + sampleQueueReaders = new DecryptableSampleQueueReader[0]; pendingResetPositionUs = C.TIME_UNSET; length = C.LENGTH_UNSET; durationUs = C.TIME_UNSET; @@ -195,6 +201,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; sampleQueue.discardToEnd(); } } + for (DecryptableSampleQueueReader reader : sampleQueueReaders) { + reader.release(); + } loader.release(/* callback= */ this); handler.removeCallbacksAndMessages(null); callback = null; @@ -432,29 +441,32 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; // SampleStream methods. /* package */ boolean isReady(int track) { - return !suppressRead() && (loadingFinished || sampleQueues[track].hasNextSample()); + return !suppressRead() && sampleQueueReaders[track].isReady(loadingFinished); + } + + /* package */ void maybeThrowError(int sampleQueueIndex) throws IOException { + sampleQueueReaders[sampleQueueIndex].maybeThrowError(); + maybeThrowError(); } /* package */ void maybeThrowError() throws IOException { loader.maybeThrowError(loadErrorHandlingPolicy.getMinimumLoadableRetryCount(dataType)); } - /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer, + /* package */ int readData( + int sampleQueueIndex, + FormatHolder formatHolder, + DecoderInputBuffer buffer, boolean formatRequired) { if (suppressRead()) { return C.RESULT_NOTHING_READ; } - maybeNotifyDownstreamFormat(track); + maybeNotifyDownstreamFormat(sampleQueueIndex); int result = - sampleQueues[track].read( - formatHolder, - buffer, - formatRequired, - /* allowOnlyClearBuffers= */ false, - loadingFinished, - lastSeekPositionUs); + sampleQueueReaders[sampleQueueIndex].read( + formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); if (result == C.RESULT_NOTHING_READ) { - maybeStartDeferredRetry(track); + maybeStartDeferredRetry(sampleQueueIndex); } return result; } @@ -667,6 +679,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @NullableType SampleQueue[] sampleQueues = Arrays.copyOf(this.sampleQueues, trackCount + 1); sampleQueues[trackCount] = trackOutput; this.sampleQueues = Util.castNonNullTypeArray(sampleQueues); + @NullableType + DecryptableSampleQueueReader[] sampleQueueReaders = + Arrays.copyOf(this.sampleQueueReaders, trackCount + 1); + sampleQueueReaders[trackCount] = + new DecryptableSampleQueueReader(this.sampleQueues[trackCount], drmSessionManager); + this.sampleQueueReaders = Util.castNonNullTypeArray(sampleQueueReaders); return trackOutput; } @@ -868,7 +886,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void maybeThrowError() throws IOException { - ProgressiveMediaPeriod.this.maybeThrowError(); + ProgressiveMediaPeriod.this.maybeThrowError(track); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java index 42ec237b3e..bd32587bdd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; @@ -51,6 +53,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource private ExtractorsFactory extractorsFactory; @Nullable private String customCacheKey; @Nullable private Object tag; + private DrmSessionManager drmSessionManager; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private int continueLoadingCheckIntervalBytes; private boolean isCreateCalled; @@ -74,6 +77,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) { this.dataSourceFactory = dataSourceFactory; this.extractorsFactory = extractorsFactory; + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; } @@ -128,6 +132,20 @@ public final class ProgressiveMediaSource extends BaseMediaSource return this; } + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManager = drmSessionManager; + return this; + } + /** * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. @@ -172,6 +190,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource uri, dataSourceFactory, extractorsFactory, + drmSessionManager, loadErrorHandlingPolicy, customCacheKey, continueLoadingCheckIntervalBytes, @@ -193,6 +212,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource private final Uri uri; private final DataSource.Factory dataSourceFactory; private final ExtractorsFactory extractorsFactory; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy; @Nullable private final String customCacheKey; private final int continueLoadingCheckIntervalBytes; @@ -207,6 +227,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy, @Nullable String customCacheKey, int continueLoadingCheckIntervalBytes, @@ -214,6 +235,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource this.uri = uri; this.dataSourceFactory = dataSourceFactory; this.extractorsFactory = extractorsFactory; + this.drmSessionManager = drmSessionManager; this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy; this.customCacheKey = customCacheKey; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; @@ -248,6 +270,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource uri, dataSource, extractorsFactory.createExtractors(), + drmSessionManager, loadableLoadErrorHandlingPolicy, createEventDispatcher(id), this, From 3fe0b1a6fee8e7631caa5a6f84306396ee6999ad Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 12 Jul 2019 18:32:58 +0100 Subject: [PATCH 1447/1556] Rename SourceInfoRefreshListener to MediaSourceCaller. This better reflects its usage as a caller identifier and not just a listener. PiperOrigin-RevId: 257827188 --- RELEASENOTES.md | 7 ++-- .../exoplayer2/ExoPlayerImplInternal.java | 5 ++- .../exoplayer2/offline/DownloadHelper.java | 5 ++- .../exoplayer2/source/BaseMediaSource.java | 21 +++++----- .../source/CompositeMediaSource.java | 25 ++++------- .../exoplayer2/source/MediaPeriod.java | 4 +- .../exoplayer2/source/MediaSource.java | 42 ++++++++----------- .../source/ConcatenatingMediaSourceTest.java | 8 ++-- .../source/dash/DashMediaSource.java | 4 +- .../testutil/MediaSourceTestRunner.java | 11 ++--- 10 files changed, 59 insertions(+), 73 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2a5e635668..12babc1688 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,9 +15,10 @@ * Add VR player demo. * Wrap decoder exceptions in a new `DecoderException` class and report as renderer error. -* Do not pass the manifest to callbacks of Player.EventListener and - SourceInfoRefreshListener anymore. Instead make it accessible through - Player.getCurrentManifest() and Timeline.Window.manifest. +* Do not pass the manifest to callbacks of `Player.EventListener` and + `SourceInfoRefreshListener` anymore. Instead make it accessible through + `Player.getCurrentManifest()` and `Timeline.Window.manifest`. Also rename + `SourceInfoRefreshListener` to `MediaSourceCaller`. * Flac extension: Parse `VORBIS_COMMENT` metadata ([#5527](https://github.com/google/ExoPlayer/issues/5527)). 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 5f53427fca..e313c9aedf 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 @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -51,7 +52,7 @@ import java.util.concurrent.atomic.AtomicBoolean; implements Handler.Callback, MediaPeriod.Callback, TrackSelector.InvalidationListener, - MediaSource.SourceInfoRefreshListener, + MediaSourceCaller, PlaybackParameterListener, PlayerMessage.Sender { @@ -264,7 +265,7 @@ import java.util.concurrent.atomic.AtomicBoolean; return internalPlaybackThread.getLooper(); } - // MediaSource.SourceInfoRefreshListener implementation. + // MediaSource.MediaSourceCaller implementation. @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 17bc304db3..139c6ad794 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.TrackGroup; @@ -800,7 +801,7 @@ public final class DownloadHelper { } private static final class MediaPreparer - implements MediaSource.SourceInfoRefreshListener, MediaPeriod.Callback, Handler.Callback { + implements MediaSourceCaller, MediaPeriod.Callback, Handler.Callback { private static final int MESSAGE_PREPARE_SOURCE = 0; private static final int MESSAGE_CHECK_FOR_FAILURE = 1; @@ -892,7 +893,7 @@ public final class DownloadHelper { } } - // MediaSource.SourceInfoRefreshListener implementation. + // MediaSource.MediaSourceCaller implementation. @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java index 124f70c64c..886952f5c3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java @@ -32,14 +32,14 @@ import java.util.ArrayList; */ public abstract class BaseMediaSource implements MediaSource { - private final ArrayList sourceInfoListeners; + private final ArrayList mediaSourceCallers; private final MediaSourceEventListener.EventDispatcher eventDispatcher; @Nullable private Looper looper; @Nullable private Timeline timeline; public BaseMediaSource() { - sourceInfoListeners = new ArrayList<>(/* initialCapacity= */ 1); + mediaSourceCallers = new ArrayList<>(/* initialCapacity= */ 1); eventDispatcher = new MediaSourceEventListener.EventDispatcher(); } @@ -67,8 +67,8 @@ public abstract class BaseMediaSource implements MediaSource { */ protected final void refreshSourceInfo(Timeline timeline) { this.timeline = timeline; - for (SourceInfoRefreshListener listener : sourceInfoListeners) { - listener.onSourceInfoRefreshed(/* source= */ this, timeline); + for (MediaSourceCaller caller : mediaSourceCallers) { + caller.onSourceInfoRefreshed(/* source= */ this, timeline); } } @@ -127,23 +127,22 @@ public abstract class BaseMediaSource implements MediaSource { @Override public final void prepareSource( - SourceInfoRefreshListener listener, - @Nullable TransferListener mediaTransferListener) { + MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) { Looper looper = Looper.myLooper(); Assertions.checkArgument(this.looper == null || this.looper == looper); - sourceInfoListeners.add(listener); + mediaSourceCallers.add(caller); if (this.looper == null) { this.looper = looper; prepareSourceInternal(mediaTransferListener); } else if (timeline != null) { - listener.onSourceInfoRefreshed(/* source= */ this, timeline); + caller.onSourceInfoRefreshed(/* source= */ this, timeline); } } @Override - public final void releaseSource(SourceInfoRefreshListener listener) { - sourceInfoListeners.remove(listener); - if (sourceInfoListeners.isEmpty()) { + public final void releaseSource(MediaSourceCaller caller) { + mediaSourceCallers.remove(caller); + if (mediaSourceCallers.isEmpty()) { looper = null; timeline = null; releaseSourceInternal(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java index 612ad33f9d..3eac3df5fe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java @@ -61,7 +61,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { @CallSuper protected void releaseSourceInternal() { for (MediaSourceAndListener childSource : childSources.values()) { - childSource.mediaSource.releaseSource(childSource.listener); + childSource.mediaSource.releaseSource(childSource.caller); childSource.mediaSource.removeEventListener(childSource.eventListener); } childSources.clear(); @@ -91,17 +91,12 @@ public abstract class CompositeMediaSource extends BaseMediaSource { */ protected final void prepareChildSource(final T id, MediaSource mediaSource) { Assertions.checkArgument(!childSources.containsKey(id)); - SourceInfoRefreshListener sourceListener = - new SourceInfoRefreshListener() { - @Override - public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { - onChildSourceInfoRefreshed(id, source, timeline); - } - }; + MediaSourceCaller caller = + (source, timeline) -> onChildSourceInfoRefreshed(id, source, timeline); MediaSourceEventListener eventListener = new ForwardingEventListener(id); - childSources.put(id, new MediaSourceAndListener(mediaSource, sourceListener, eventListener)); + childSources.put(id, new MediaSourceAndListener(mediaSource, caller, eventListener)); mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener); - mediaSource.prepareSource(sourceListener, mediaTransferListener); + mediaSource.prepareSource(caller, mediaTransferListener); } /** @@ -111,7 +106,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { */ protected final void releaseChildSource(T id) { MediaSourceAndListener removedChild = Assertions.checkNotNull(childSources.remove(id)); - removedChild.mediaSource.releaseSource(removedChild.listener); + removedChild.mediaSource.releaseSource(removedChild.caller); removedChild.mediaSource.removeEventListener(removedChild.eventListener); } @@ -157,15 +152,13 @@ public abstract class CompositeMediaSource extends BaseMediaSource { private static final class MediaSourceAndListener { public final MediaSource mediaSource; - public final SourceInfoRefreshListener listener; + public final MediaSourceCaller caller; public final MediaSourceEventListener eventListener; public MediaSourceAndListener( - MediaSource mediaSource, - SourceInfoRefreshListener listener, - MediaSourceEventListener eventListener) { + MediaSource mediaSource, MediaSourceCaller caller, MediaSourceEventListener eventListener) { this.mediaSource = mediaSource; - this.listener = listener; + this.caller = caller; this.eventListener = eventListener; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index f86be8afc2..f076eae32c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.trackselection.TrackSelection; import java.io.IOException; import java.util.Collections; @@ -57,8 +58,7 @@ public interface MediaPeriod extends SequenceableLoader { * {@link #maybeThrowPrepareError()} will throw an {@link IOException}. * *

        If preparation succeeds and results in a source timeline change (e.g. the period duration - * becoming known), {@link - * MediaSource.SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline)} will be + * becoming known), {@link MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} will be * called before {@code callback.onPrepared}. * * @param callback Callback to receive updates from this period, including being notified when diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index bd87eb6509..c3219a03c1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -30,9 +30,9 @@ import java.io.IOException; *

          *
        • To provide the player with a {@link Timeline} defining the structure of its media, and to * provide a new timeline whenever the structure of the media changes. The MediaSource - * provides these timelines by calling {@link SourceInfoRefreshListener#onSourceInfoRefreshed} - * on the {@link SourceInfoRefreshListener}s passed to {@link - * #prepareSource(SourceInfoRefreshListener, TransferListener)}. + * provides these timelines by calling {@link MediaSourceCaller#onSourceInfoRefreshed} on the + * {@link MediaSourceCaller}s passed to {@link #prepareSource(MediaSourceCaller, + * TransferListener)}. *
        • To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator, long)}, and provide a * way for the player to load and read the media. @@ -45,25 +45,21 @@ import java.io.IOException; */ public interface MediaSource { - /** Listener for source events. */ - interface SourceInfoRefreshListener { + /** A caller of media sources, which will be notified of source events. */ + interface MediaSourceCaller { /** - * Called when the timeline has been refreshed. + * Called when the {@link Timeline} has been refreshed. * *

          Called on the playback thread. * * @param source The {@link MediaSource} whose info has been refreshed. * @param timeline The source's timeline. */ - default void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { - // Do nothing. - } + void onSourceInfoRefreshed(MediaSource source, Timeline timeline); } - /** - * Identifier for a {@link MediaPeriod}. - */ + /** Identifier for a {@link MediaPeriod}. */ final class MediaPeriodId { /** The unique id of the timeline period. */ @@ -239,24 +235,23 @@ public interface MediaSource { } /** - * Starts source preparation. + * Registers a {@link MediaSourceCaller} and starts source preparation if needed. * *

          Should not be called directly from application code. * - *

          {@link SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline)} will be - * called once the source has a {@link Timeline}. + *

          {@link MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} will be called once + * the source has a {@link Timeline}. * - *

          For each call to this method, a call to {@link #releaseSource(SourceInfoRefreshListener)} is - * needed to remove the listener and to release the source if no longer required. + *

          For each call to this method, a call to {@link #releaseSource(MediaSourceCaller)} is needed + * to remove the caller and to release the source if no longer required. * - * @param listener The listener to be added. + * @param caller The {@link MediaSourceCaller} to be registered. * @param mediaTransferListener The transfer listener which should be informed of any media data * transfers. May be null if no listener is available. Note that this listener should be only * informed of transfers related to the media loads and not of auxiliary loads for manifests * and other data. */ - void prepareSource( - SourceInfoRefreshListener listener, @Nullable TransferListener mediaTransferListener); + void prepareSource(MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener); /** * Throws any pending error encountered while loading or refreshing source information. @@ -288,12 +283,11 @@ public interface MediaSource { void releasePeriod(MediaPeriod mediaPeriod); /** - * Removes a listener for timeline and/or manifest updates and releases the source if no longer - * required. + * Unregisters a caller and releases the source if no longer required. * *

          Should not be called directly from application code. * - * @param listener The listener to be removed. + * @param caller The {@link MediaSourceCaller} to be unregistered. */ - void releaseSource(SourceInfoRefreshListener listener); + void releaseSource(MediaSourceCaller caller); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index c587d85a85..39f36a991b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.source.MediaSource.SourceInfoRefreshListener; +import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeMediaSource; @@ -628,15 +628,15 @@ public final class ConcatenatingMediaSourceTest { try { dummyMainThread.runOnMainThread( () -> { - SourceInfoRefreshListener listener = mock(SourceInfoRefreshListener.class); + MediaSourceCaller caller = mock(MediaSourceCaller.class); mediaSource.addMediaSources(Arrays.asList(createMediaSources(2))); - mediaSource.prepareSource(listener, /* mediaTransferListener= */ null); + mediaSource.prepareSource(caller, /* mediaTransferListener= */ null); mediaSource.moveMediaSource( /* currentIndex= */ 0, /* newIndex= */ 1, new Handler(), callbackCalledCondition::open); - mediaSource.releaseSource(listener); + mediaSource.releaseSource(caller); }); assertThat(callbackCalledCondition.block(MediaSourceTestRunner.TIMEOUT_MS)).isTrue(); } finally { 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 f7387ee77c..576491b464 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 @@ -366,8 +366,8 @@ public final class DashMediaSource extends BaseMediaSource { /** * The interval in milliseconds between invocations of {@link - * SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline)} when the source's - * {@link Timeline} is changing dynamically (for example, for incomplete live streams). + * MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} when the source's {@link + * Timeline} is changing dynamically (for example, for incomplete live streams). */ private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000; /** diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 6d626088fc..211e85d30c 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; @@ -199,10 +200,7 @@ public class MediaSourceTestRunner { runOnPlaybackThread(() -> mediaSource.releasePeriod(mediaPeriod)); } - /** - * Calls {@link MediaSource#releaseSource(MediaSource.SourceInfoRefreshListener)} on the playback - * thread. - */ + /** Calls {@link MediaSource#releaseSource(MediaSourceCaller)} on the playback thread. */ public void releaseSource() { runOnPlaybackThread(() -> mediaSource.releaseSource(mediaSourceListener)); } @@ -339,10 +337,9 @@ public class MediaSourceTestRunner { playbackThread.quit(); } - private class MediaSourceListener - implements MediaSource.SourceInfoRefreshListener, MediaSourceEventListener { + private class MediaSourceListener implements MediaSourceCaller, MediaSourceEventListener { - // SourceInfoRefreshListener methods. + // MediaSourceCaller methods. @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { From bbcd1126b214596006b35fe104d7a3a78ba2ae5a Mon Sep 17 00:00:00 2001 From: tonihei Date: Sun, 14 Jul 2019 15:36:00 +0100 Subject: [PATCH 1448/1556] Remove DownloadService from nullness blacklist. PiperOrigin-RevId: 258038961 --- .../exoplayer2/offline/DownloadService.java | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 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 3900dc8e93..84b6f5870d 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 @@ -175,7 +175,7 @@ public abstract class DownloadService extends Service { @Nullable private final String channelId; @StringRes private final int channelNameResourceId; - private DownloadManager downloadManager; + @Nullable private DownloadManager downloadManager; private int lastStartId; private boolean startedInForeground; private boolean taskRemoved; @@ -575,6 +575,7 @@ public abstract class DownloadService extends Service { if (intentAction == null) { intentAction = ACTION_INIT; } + DownloadManager downloadManager = Assertions.checkNotNull(this.downloadManager); switch (intentAction) { case ACTION_INIT: case ACTION_RESTART: @@ -640,8 +641,9 @@ public abstract class DownloadService extends Service { @Override public void onDestroy() { isDestroyed = true; - DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(getClass()); - boolean unschedule = !downloadManager.isWaitingForRequirements(); + DownloadManagerHelper downloadManagerHelper = + Assertions.checkNotNull(downloadManagerListeners.get(getClass())); + boolean unschedule = !downloadManagerHelper.downloadManager.isWaitingForRequirements(); downloadManagerHelper.detachService(this, unschedule); if (foregroundNotificationUpdater != null) { foregroundNotificationUpdater.stopPeriodicUpdates(); @@ -775,7 +777,6 @@ public abstract class DownloadService extends Service { private final int notificationId; private final long updateInterval; private final Handler handler; - private final Runnable updateRunnable; private boolean periodicUpdatesStarted; private boolean notificationDisplayed; @@ -784,7 +785,6 @@ public abstract class DownloadService extends Service { this.notificationId = notificationId; this.updateInterval = updateInterval; this.handler = new Handler(Looper.getMainLooper()); - this.updateRunnable = this::update; } public void startPeriodicUpdates() { @@ -794,7 +794,7 @@ public abstract class DownloadService extends Service { public void stopPeriodicUpdates() { periodicUpdatesStarted = false; - handler.removeCallbacks(updateRunnable); + handler.removeCallbacksAndMessages(null); } public void showNotificationIfNotAlready() { @@ -810,12 +810,12 @@ public abstract class DownloadService extends Service { } private void update() { - List downloads = downloadManager.getCurrentDownloads(); + List downloads = Assertions.checkNotNull(downloadManager).getCurrentDownloads(); startForeground(notificationId, getForegroundNotification(downloads)); notificationDisplayed = true; if (periodicUpdatesStarted) { - handler.removeCallbacks(updateRunnable); - handler.postDelayed(updateRunnable, updateInterval); + handler.removeCallbacksAndMessages(null); + handler.postDelayed(this::update, updateInterval); } } } @@ -840,7 +840,8 @@ public abstract class DownloadService extends Service { downloadManager.addListener(this); if (scheduler != null) { Requirements requirements = downloadManager.getRequirements(); - setSchedulerEnabled(/* enabled= */ !requirements.checkRequirements(context), requirements); + setSchedulerEnabled( + scheduler, /* enabled= */ !requirements.checkRequirements(context), requirements); } } @@ -894,11 +895,12 @@ public abstract class DownloadService extends Service { } } if (scheduler != null) { - setSchedulerEnabled(/* enabled= */ !requirementsMet, requirements); + setSchedulerEnabled(scheduler, /* enabled= */ !requirementsMet, requirements); } } - private void setSchedulerEnabled(boolean enabled, Requirements requirements) { + private void setSchedulerEnabled( + Scheduler scheduler, boolean enabled, Requirements requirements) { if (!enabled) { scheduler.cancel(); } else { From af98883a7bffc6b8a760b84737012ddd5cfcd622 Mon Sep 17 00:00:00 2001 From: Yannick RUI Date: Mon, 15 Jul 2019 11:35:32 +0200 Subject: [PATCH 1449/1556] Reintroducing existing logic as requested here https://github.com/google/ExoPlayer/pull/6178#pullrequestreview-261298162 --- .../trackselection/DefaultTrackSelector.java | 131 ++++++++++-------- 1 file changed, 75 insertions(+), 56 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 511a974a0e..c0de66516b 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 @@ -1555,29 +1555,39 @@ public class DefaultTrackSelector extends MappingTrackSelector { TextTrackScore selectedTextTrackScore = null; int selectedTextRendererIndex = C.INDEX_UNSET; for (int i = 0; i < rendererCount; i++) { - // The below behaviour is different from video and audio track selection - // i.e. do not perform a text track pre selection if there are no preferredTextLanguage requested. - if (C.TRACK_TYPE_TEXT == mappedTrackInfo.getRendererType(i) && params.preferredTextLanguage != null) { - Pair textSelection = - selectTextTrack( - mappedTrackInfo.getTrackGroups(i), - rendererFormatSupports[i], - params); - if (textSelection != null - && (selectedTextTrackScore == null - || textSelection.second.compareTo(selectedTextTrackScore) > 0)) { - if (selectedTextRendererIndex != C.INDEX_UNSET) { - // We've already made a selection for another text renderer, but it had a lower - // score. Clear the selection for that renderer. - definitions[selectedTextRendererIndex] = null; + int trackType = mappedTrackInfo.getRendererType(i); + switch (trackType) { + case C.TRACK_TYPE_VIDEO: + case C.TRACK_TYPE_AUDIO: + // Already done. Do nothing. + break; + case C.TRACK_TYPE_TEXT: + Pair textSelection = + selectTextTrack( + mappedTrackInfo.getTrackGroups(i), + rendererFormatSupports[i], + params, + selectedAudioLanguage); + if (textSelection != null + && (selectedTextTrackScore == null + || textSelection.second.compareTo(selectedTextTrackScore) > 0)) { + if (selectedTextRendererIndex != C.INDEX_UNSET) { + // We've already made a selection for another text renderer, but it had a lower + // score. Clear the selection for that renderer. + definitions[selectedTextRendererIndex] = null; + } + definitions[i] = textSelection.first; + selectedTextTrackScore = textSelection.second; + selectedTextRendererIndex = i; } - TrackSelection.Definition definition = textSelection.first; - definitions[i] = definition; - selectedTextTrackScore = textSelection.second; - selectedTextRendererIndex = i; + break; + default: + definitions[i] = + selectOtherTrack( + trackType, mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params); + break; } } - } return definitions; } @@ -2052,7 +2062,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { protected Pair selectTextTrack( TrackGroupArray groups, int[][] formatSupport, - Parameters params) + Parameters params, + @Nullable String selectedAudioLanguage) throws ExoPlaybackException { TrackGroup selectedGroup = null; int selectedTrackIndex = C.INDEX_UNSET; @@ -2064,7 +2075,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); - TextTrackScore trackScore = new TextTrackScore(format, params, trackFormatSupport[trackIndex]); + TextTrackScore trackScore = new TextTrackScore(format, params, trackFormatSupport[trackIndex], selectedAudioLanguage); if ((selectedTrackScore == null) || trackScore.compareTo(selectedTrackScore) > 0) { selectedGroup = trackGroup; selectedTrackIndex = trackIndex; @@ -2497,29 +2508,49 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** Represents how well an text track matches the selection {@link Parameters}. */ protected static final class TextTrackScore implements Comparable { - private final boolean isWithinRendererCapabilities; - private final int preferredLanguageScore; - private final int localeLanguageMatchIndex; - private final int localeLanguageScore; - private final boolean isDefaultSelectionFlag; + private final boolean isDefault; + private final boolean isForced; + private final int languageScore; + private final boolean trackHasNoLanguage; + private int bestMatchScore = 0; - public TextTrackScore(Format format, Parameters parameters, int formatSupport) { - isWithinRendererCapabilities = isSupported(formatSupport, false); - preferredLanguageScore = getFormatLanguageScore(format, parameters.preferredTextLanguage); - isDefaultSelectionFlag = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; - String[] localeLanguages = Util.getSystemLanguageCodes(); - int bestMatchIndex = Integer.MAX_VALUE; - int bestMatchScore = 0; - for (int i = 0; i < localeLanguages.length; i++) { - int score = getFormatLanguageScore(format, localeLanguages[i]); - if (score > 0) { - bestMatchIndex = i; - bestMatchScore = score; - break; + public TextTrackScore( + Format format, + Parameters parameters, + int trackFormatSupport, + @Nullable String selectedAudioLanguage) { + languageScore = getFormatLanguageScore(format, parameters.preferredTextLanguage); + int maskedSelectionFlags = + format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags; + isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; + isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; + trackHasNoLanguage = formatHasNoLanguage(format); + + if (languageScore > 0 || (parameters.selectUndeterminedTextLanguage && trackHasNoLanguage)) { + if (isDefault) { + bestMatchScore = 11; + } else if (!isForced) { + // Prefer non-forced to forced if a preferred text language has been specified. Where + // both are provided the non-forced track will usually contain the forced subtitles as + // a subset. + bestMatchScore = 7; + } else { + bestMatchScore = 3; } + bestMatchScore += languageScore; + } else if (isDefault) { + bestMatchScore = 2; + } else if (isForced + && (languageScore > 0 + || (trackHasNoLanguage && stringDefinesNoLanguage(selectedAudioLanguage)))) { + bestMatchScore = 1; + } else { + // Track should not be selected. + bestMatchScore = -1; + } + if (isSupported(trackFormatSupport, false)) { + bestMatchScore += WITHIN_RENDERER_CAPABILITIES_BONUS; } - localeLanguageMatchIndex = bestMatchIndex; - localeLanguageScore = bestMatchScore; } /** @@ -2531,20 +2562,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ @Override public int compareTo(@NonNull TextTrackScore other) { - if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { - return this.isWithinRendererCapabilities ? 1 : -1; - } - if (this.preferredLanguageScore != other.preferredLanguageScore) { - return compareInts(this.preferredLanguageScore, other.preferredLanguageScore); - } - if (this.isDefaultSelectionFlag != other.isDefaultSelectionFlag) { - return this.isDefaultSelectionFlag ? 1 : -1; - } - if (this.localeLanguageMatchIndex != other.localeLanguageMatchIndex) { - return -compareInts(this.localeLanguageMatchIndex, other.localeLanguageMatchIndex); - } - if (this.localeLanguageScore != other.localeLanguageScore) { - return compareInts(this.localeLanguageScore, other.localeLanguageScore); + if (this.bestMatchScore != other.bestMatchScore) { + return compareInts(this.bestMatchScore, other.bestMatchScore); } return 0; } From 1909987dc8d63ef6504d7c872e3ce28b9cc560c7 Mon Sep 17 00:00:00 2001 From: Yannick RUI Date: Mon, 15 Jul 2019 16:05:01 +0200 Subject: [PATCH 1450/1556] Change all the conditions and scores that are currently in the constructor of TextTrackScore to comparisons in TextTrackScore.compareTo --- .../trackselection/DefaultTrackSelector.java | 73 +++++++++++-------- 1 file changed, 41 insertions(+), 32 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 c0de66516b..b5d282f8a7 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 @@ -2508,49 +2508,30 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** Represents how well an text track matches the selection {@link Parameters}. */ protected static final class TextTrackScore implements Comparable { + private final boolean isWithinRendererCapabilities; private final boolean isDefault; private final boolean isForced; - private final int languageScore; + private final int preferredLanguageScore; + private final int selectedAudioLanguageScore; private final boolean trackHasNoLanguage; - private int bestMatchScore = 0; + private final boolean selectUndeterminedTextLanguage; + private final boolean stringDefinesNoLang; public TextTrackScore( Format format, Parameters parameters, int trackFormatSupport, @Nullable String selectedAudioLanguage) { - languageScore = getFormatLanguageScore(format, parameters.preferredTextLanguage); + isWithinRendererCapabilities = isSupported(trackFormatSupport, false); int maskedSelectionFlags = format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags; isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; + preferredLanguageScore = getFormatLanguageScore(format, parameters.preferredTextLanguage); + selectedAudioLanguageScore = getFormatLanguageScore(format, selectedAudioLanguage); trackHasNoLanguage = formatHasNoLanguage(format); - - if (languageScore > 0 || (parameters.selectUndeterminedTextLanguage && trackHasNoLanguage)) { - if (isDefault) { - bestMatchScore = 11; - } else if (!isForced) { - // Prefer non-forced to forced if a preferred text language has been specified. Where - // both are provided the non-forced track will usually contain the forced subtitles as - // a subset. - bestMatchScore = 7; - } else { - bestMatchScore = 3; - } - bestMatchScore += languageScore; - } else if (isDefault) { - bestMatchScore = 2; - } else if (isForced - && (languageScore > 0 - || (trackHasNoLanguage && stringDefinesNoLanguage(selectedAudioLanguage)))) { - bestMatchScore = 1; - } else { - // Track should not be selected. - bestMatchScore = -1; - } - if (isSupported(trackFormatSupport, false)) { - bestMatchScore += WITHIN_RENDERER_CAPABILITIES_BONUS; - } + selectUndeterminedTextLanguage = parameters.selectUndeterminedTextLanguage; + stringDefinesNoLang = stringDefinesNoLanguage(selectedAudioLanguage); } /** @@ -2562,10 +2543,38 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ @Override public int compareTo(@NonNull TextTrackScore other) { - if (this.bestMatchScore != other.bestMatchScore) { - return compareInts(this.bestMatchScore, other.bestMatchScore); + if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { + return this.isWithinRendererCapabilities ? 1 : -1; + } + if ((this.preferredLanguageScore > 0 || (this.selectUndeterminedTextLanguage && this.trackHasNoLanguage)) == + (other.preferredLanguageScore > 0 || (other.selectUndeterminedTextLanguage && other.trackHasNoLanguage))) { + if (this.preferredLanguageScore > 0 || (this.selectUndeterminedTextLanguage + && this.trackHasNoLanguage)) { + if (this.isDefault != other.isDefault) { + return this.isDefault ? 1 : -1; + } + if (this.isForced != other.isForced) { + // Prefer non-forced to forced if a preferred text language has been specified. Where + // both are provided the non-forced track will usually contain the forced subtitles as + // a subset. + return !this.isForced ? 1 : -1; + } + return (this.preferredLanguageScore > other.preferredLanguageScore) ? 1 : -1; + } else { + if (this.isDefault != other.isDefault) { + return this.isDefault ? 1 : -1; + } + if ((this.isForced && (this.selectedAudioLanguageScore > 0 || (this.trackHasNoLanguage && this.stringDefinesNoLang))) != + (other.isForced && (other.selectedAudioLanguageScore > 0 || (other.trackHasNoLanguage && other.stringDefinesNoLang)))) { + return (this.isForced && (this.selectedAudioLanguageScore > 0 + || (this.trackHasNoLanguage && this.stringDefinesNoLang))) ? 1 : -1; + } + // Track should not be selected. + return -1; + } + } else { + return (this.preferredLanguageScore > 0 || (this.selectUndeterminedTextLanguage && this.trackHasNoLanguage)) ? 1 : -1; } - return 0; } } } From 1d4d10517448e5428e7c2db969d1a8c330938be6 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 15 Jul 2019 09:04:20 +0100 Subject: [PATCH 1451/1556] Compile with SDK 29 (Android Q) PiperOrigin-RevId: 258110603 --- RELEASENOTES.md | 1 + constants.gradle | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 12babc1688..46515d8fa8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -21,6 +21,7 @@ `SourceInfoRefreshListener` to `MediaSourceCaller`. * Flac extension: Parse `VORBIS_COMMENT` metadata ([#5527](https://github.com/google/ExoPlayer/issues/5527)). +* Set `compileSdkVersion` to 29 to use Android Q APIs. ### 2.10.3 ### diff --git a/constants.gradle b/constants.gradle index e857d5a812..c8136ea471 100644 --- a/constants.gradle +++ b/constants.gradle @@ -17,7 +17,7 @@ project.ext { releaseVersionCode = 2010003 minSdkVersion = 16 targetSdkVersion = 28 - compileSdkVersion = 28 + compileSdkVersion = 29 dexmakerVersion = '2.21.0' mockitoVersion = '2.25.0' robolectricVersion = '4.3' From 2b5c42e02770475c72e45c887f9f2db8a688db4f Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 15 Jul 2019 15:48:09 +0100 Subject: [PATCH 1452/1556] Fix some inline parameter name comments. The name of this parameter recently changed in https://github.com/google/ExoPlayer/commit/3fe0b1a6fee8e7631caa5a6f84306396ee6999ad and I forgot to change these inline comment usages. PiperOrigin-RevId: 258160659 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 4 ++-- .../com/google/android/exoplayer2/offline/DownloadHelper.java | 2 +- 2 files changed, 3 insertions(+), 3 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 e313c9aedf..0fc7242279 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 @@ -442,7 +442,7 @@ import java.util.concurrent.atomic.AtomicBoolean; loadControl.onPrepared(); this.mediaSource = mediaSource; setState(Player.STATE_BUFFERING); - mediaSource.prepareSource(/* listener= */ this, bandwidthMeter.getTransferListener()); + mediaSource.prepareSource(/* caller= */ this, bandwidthMeter.getTransferListener()); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -914,7 +914,7 @@ import java.util.concurrent.atomic.AtomicBoolean; startPositionUs); if (releaseMediaSource) { if (mediaSource != null) { - mediaSource.releaseSource(/* listener= */ this); + mediaSource.releaseSource(/* caller= */ this); mediaSource = null; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 139c6ad794..4b5bf3c8a4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -852,7 +852,7 @@ public final class DownloadHelper { public boolean handleMessage(Message msg) { switch (msg.what) { case MESSAGE_PREPARE_SOURCE: - mediaSource.prepareSource(/* listener= */ this, /* mediaTransferListener= */ null); + mediaSource.prepareSource(/* caller= */ this, /* mediaTransferListener= */ null); mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE); return true; case MESSAGE_CHECK_FOR_FAILURE: From 7760eca238d68c2729bb95747bf01e64cbb2fd7b Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 16 Jul 2019 03:30:40 +0100 Subject: [PATCH 1453/1556] Deep compare formats in SampleMetadataQueue instead of shallow compare PiperOrigin-RevId: 258285645 --- .../android/exoplayer2/source/SampleMetadataQueue.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index 542565e70d..78b3a35549 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -227,7 +227,7 @@ import com.google.android.exoplayer2.util.Util; return SampleQueue.PEEK_RESULT_NOTHING; } int relativeReadIndex = getRelativeIndex(readPosition); - if (formats[relativeReadIndex] != downstreamFormat) { + if (formats[relativeReadIndex].equals(downstreamFormat)) { return SampleQueue.PEEK_RESULT_FORMAT; } else { return (flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0 @@ -275,7 +275,7 @@ import com.google.android.exoplayer2.util.Util; buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); return C.RESULT_BUFFER_READ; } else if (upstreamFormat != null - && (formatRequired || upstreamFormat != downstreamFormat)) { + && (formatRequired || !upstreamFormat.equals(downstreamFormat))) { formatHolder.format = upstreamFormat; return C.RESULT_FORMAT_READ; } else { @@ -284,7 +284,7 @@ import com.google.android.exoplayer2.util.Util; } int relativeReadIndex = getRelativeIndex(readPosition); - if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { + if (formatRequired || !formats[relativeReadIndex].equals(downstreamFormat)) { formatHolder.format = formats[relativeReadIndex]; return C.RESULT_FORMAT_READ; } @@ -422,7 +422,6 @@ import com.google.android.exoplayer2.util.Util; } upstreamFormatRequired = false; if (Util.areEqual(format, upstreamFormat)) { - // Suppress changes between equal formats so we can use referential equality in readData. return false; } else { upstreamFormat = format; From 09147ff5481e9741a29534a795d99a3bda486a72 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 16 Jul 2019 13:03:10 +0100 Subject: [PATCH 1454/1556] Add missing consts for consistency These getters do not modify the instance. PiperOrigin-RevId: 258345084 --- extensions/flac/src/main/jni/include/flac_parser.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index d9043e9548..f09a22e951 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -48,9 +48,11 @@ class FLACParser { return mStreamInfo; } - bool isVorbisCommentsValid() { return mVorbisCommentsValid; } + bool isVorbisCommentsValid() const { return mVorbisCommentsValid; } - std::vector getVorbisComments() { return mVorbisComments; } + const std::vector& getVorbisComments() const { + return mVorbisComments; + } int64_t getLastFrameTimestamp() const { return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); From 01376443b36f70c1fe377e4898959dfa34f6024a Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 16 Jul 2019 16:39:23 +0100 Subject: [PATCH 1455/1556] Add MediaSource.enable/disable. These methods helps to indicate that a media source isn't used to create new periods in the immediate term and thus limited resources can be released. PiperOrigin-RevId: 258373069 --- RELEASENOTES.md | 2 + .../exoplayer2/source/BaseMediaSource.java | 46 ++++++++++++++++-- .../source/CompositeMediaSource.java | 41 +++++++++++++++- .../source/ConcatenatingMediaSource.java | 48 ++++++++++++++++++- .../exoplayer2/source/MediaSource.java | 48 +++++++++++++++---- 5 files changed, 171 insertions(+), 14 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 46515d8fa8..9d66a6f800 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,8 @@ * Flac extension: Parse `VORBIS_COMMENT` metadata ([#5527](https://github.com/google/ExoPlayer/issues/5527)). * Set `compileSdkVersion` to 29 to use Android Q APIs. +* Add `enable` and `disable` methods to `MediaSource` to improve resource + management in playlists. ### 2.10.3 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java index 886952f5c3..86e00e0a37 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import java.util.ArrayList; +import java.util.HashSet; /** * Base {@link MediaSource} implementation to handle parallel reuse and to keep a list of {@link @@ -33,6 +34,7 @@ import java.util.ArrayList; public abstract class BaseMediaSource implements MediaSource { private final ArrayList mediaSourceCallers; + private final HashSet enabledMediaSourceCallers; private final MediaSourceEventListener.EventDispatcher eventDispatcher; @Nullable private Looper looper; @@ -40,11 +42,13 @@ public abstract class BaseMediaSource implements MediaSource { public BaseMediaSource() { mediaSourceCallers = new ArrayList<>(/* initialCapacity= */ 1); + enabledMediaSourceCallers = new HashSet<>(/* initialCapacity= */ 1); eventDispatcher = new MediaSourceEventListener.EventDispatcher(); } /** - * Starts source preparation. This method is called at most once until the next call to {@link + * Starts source preparation and enables the source, see {@link #prepareSource(MediaSourceCaller, + * TransferListener)}. This method is called at most once until the next call to {@link * #releaseSourceInternal()}. * * @param mediaTransferListener The transfer listener which should be informed of any media data @@ -54,9 +58,15 @@ public abstract class BaseMediaSource implements MediaSource { */ protected abstract void prepareSourceInternal(@Nullable TransferListener mediaTransferListener); + /** Enables the source, see {@link #enable(MediaSourceCaller)}. */ + protected void enableInternal() {} + + /** Disables the source, see {@link #disable(MediaSourceCaller)}. */ + protected void disableInternal() {} + /** - * Releases the source. This method is called exactly once after each call to {@link - * #prepareSourceInternal(TransferListener)}. + * Releases the source, see {@link #releaseSource(MediaSourceCaller)}. This method is called + * exactly once after each call to {@link #prepareSourceInternal(TransferListener)}. */ protected abstract void releaseSourceInternal(); @@ -115,6 +125,11 @@ public abstract class BaseMediaSource implements MediaSource { return eventDispatcher.withParameters(windowIndex, mediaPeriodId, mediaTimeOffsetMs); } + /** Returns whether the source is enabled. */ + protected final boolean isEnabled() { + return !enabledMediaSourceCallers.isEmpty(); + } + @Override public final void addEventListener(Handler handler, MediaSourceEventListener eventListener) { eventDispatcher.addEventListener(handler, eventListener); @@ -130,22 +145,47 @@ public abstract class BaseMediaSource implements MediaSource { MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) { Looper looper = Looper.myLooper(); Assertions.checkArgument(this.looper == null || this.looper == looper); + Timeline timeline = this.timeline; mediaSourceCallers.add(caller); if (this.looper == null) { this.looper = looper; + enabledMediaSourceCallers.add(caller); prepareSourceInternal(mediaTransferListener); } else if (timeline != null) { + enable(caller); caller.onSourceInfoRefreshed(/* source= */ this, timeline); } } + @Override + public final void enable(MediaSourceCaller caller) { + Assertions.checkNotNull(looper); + boolean wasDisabled = enabledMediaSourceCallers.isEmpty(); + enabledMediaSourceCallers.add(caller); + if (wasDisabled) { + enableInternal(); + } + } + + @Override + public final void disable(MediaSourceCaller caller) { + boolean wasEnabled = !enabledMediaSourceCallers.isEmpty(); + enabledMediaSourceCallers.remove(caller); + if (wasEnabled && enabledMediaSourceCallers.isEmpty()) { + disableInternal(); + } + } + @Override public final void releaseSource(MediaSourceCaller caller) { mediaSourceCallers.remove(caller); if (mediaSourceCallers.isEmpty()) { looper = null; timeline = null; + enabledMediaSourceCallers.clear(); releaseSourceInternal(); + } else { + disable(caller); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java index 3eac3df5fe..3672c304cc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java @@ -37,7 +37,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { @Nullable private Handler eventHandler; @Nullable private TransferListener mediaTransferListener; - /** Create composite media source without child sources. */ + /** Creates composite media source without child sources. */ protected CompositeMediaSource() { childSources = new HashMap<>(); } @@ -57,6 +57,22 @@ public abstract class CompositeMediaSource extends BaseMediaSource { } } + @Override + @CallSuper + protected void enableInternal() { + for (MediaSourceAndListener childSource : childSources.values()) { + childSource.mediaSource.enable(childSource.caller); + } + } + + @Override + @CallSuper + protected void disableInternal() { + for (MediaSourceAndListener childSource : childSources.values()) { + childSource.mediaSource.disable(childSource.caller); + } + } + @Override @CallSuper protected void releaseSourceInternal() { @@ -97,6 +113,29 @@ public abstract class CompositeMediaSource extends BaseMediaSource { childSources.put(id, new MediaSourceAndListener(mediaSource, caller, eventListener)); mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener); mediaSource.prepareSource(caller, mediaTransferListener); + if (!isEnabled()) { + mediaSource.disable(caller); + } + } + + /** + * Enables a child source. + * + * @param id The unique id used to prepare the child source. + */ + protected final void enableChildSource(final T id) { + MediaSourceAndListener enabledChild = Assertions.checkNotNull(childSources.get(id)); + enabledChild.mediaSource.enable(enabledChild.caller); + } + + /** + * Disables a child source. + * + * @param id The unique id used to prepare the child source. + */ + protected final void disableChildSource(final T id) { + MediaSourceAndListener disabledChild = Assertions.checkNotNull(childSources.get(id)); + disabledChild.mediaSource.disable(disabledChild.caller); } /** 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 18d5c49fb4..669a0e7bb4 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 @@ -35,6 +35,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -68,6 +69,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceHolders; private final Map mediaSourceByMediaPeriod; private final Map mediaSourceByUid; + private final Set enabledMediaSourceHolders; private final boolean isAtomic; private final boolean useLazyPreparation; @@ -131,6 +133,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource(); this.nextTimelineUpdateOnCompletionActions = new HashSet<>(); this.pendingOnCompletionActions = new HashSet<>(); + this.enabledMediaSourceHolders = new HashSet<>(); this.isAtomic = isAtomic; this.useLazyPreparation = useLazyPreparation; addMediaSources(Arrays.asList(mediaSources)); @@ -418,7 +421,8 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource iterator = enabledMediaSourceHolders.iterator(); + while (iterator.hasNext()) { + MediaSourceHolder holder = iterator.next(); + if (holder.activeMediaPeriodIds.isEmpty()) { + disableChildSource(holder); + iterator.remove(); + } + } + } + /** Return uid of media source holder from period uid of concatenated source. */ private static Object getMediaSourceHolderUid(Object periodUid) { return ConcatenatedTimeline.getChildTimelineUidFromConcatenatedUid(periodUid); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index c3219a03c1..5ee980d01f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -235,7 +235,8 @@ public interface MediaSource { } /** - * Registers a {@link MediaSourceCaller} and starts source preparation if needed. + * Registers a {@link MediaSourceCaller}. Starts source preparation if needed and enables the + * source for the creation of {@link MediaPeriod MediaPerods}. * *

          Should not be called directly from application code. * @@ -255,17 +256,31 @@ public interface MediaSource { /** * Throws any pending error encountered while loading or refreshing source information. - *

          - * Should not be called directly from application code. + * + *

          Should not be called directly from application code. + * + *

          Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener)}. */ void maybeThrowSourceInfoRefreshError() throws IOException; /** - * Returns a new {@link MediaPeriod} identified by {@code periodId}. This method may be called - * multiple times without an intervening call to {@link #releasePeriod(MediaPeriod)}. + * Enables the source for the creation of {@link MediaPeriod MediaPeriods}. * *

          Should not be called directly from application code. * + *

          Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener)}. + * + * @param caller The {@link MediaSourceCaller} enabling the source. + */ + void enable(MediaSourceCaller caller); + + /** + * Returns a new {@link MediaPeriod} identified by {@code periodId}. + * + *

          Should not be called directly from application code. + * + *

          Must only be called if the source is enabled. + * * @param id The identifier of the period. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param startPositionUs The expected start position, in microseconds. @@ -275,18 +290,35 @@ public interface MediaSource { /** * Releases the period. - *

          - * Should not be called directly from application code. + * + *

          Should not be called directly from application code. * * @param mediaPeriod The period to release. */ void releasePeriod(MediaPeriod mediaPeriod); /** - * Unregisters a caller and releases the source if no longer required. + * Disables the source for the creation of {@link MediaPeriod MediaPeriods}. The implementation + * should not hold onto limited resources used for the creation of media periods. * *

          Should not be called directly from application code. * + *

          Must only be called after all {@link MediaPeriod MediaPeriods} previously created by {@link + * #createPeriod(MediaPeriodId, Allocator, long)} have been released by {@link + * #releasePeriod(MediaPeriod)}. + * + * @param caller The {@link MediaSourceCaller} disabling the source. + */ + void disable(MediaSourceCaller caller); + + /** + * Unregisters a caller, and disables and releases the source if no longer required. + * + *

          Should not be called directly from application code. + * + *

          Must only be called if all created {@link MediaPeriod MediaPeriods} have been released by + * {@link #releasePeriod(MediaPeriod)}. + * * @param caller The {@link MediaSourceCaller} to be unregistered. */ void releaseSource(MediaSourceCaller caller); From 5e4f52541d668f5a8950a80b28f6a8bbcaca83b1 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Jul 2019 10:10:39 +0100 Subject: [PATCH 1456/1556] Extend RK video_decoder workaround to newer API levels Issue: #6184 PiperOrigin-RevId: 258527533 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 5 ++--- 1 file changed, 2 insertions(+), 3 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 30083cb849..974e033b67 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 @@ -1856,9 +1856,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ 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))) + return (Util.SDK_INT <= 25 && "OMX.rk.video_decoder.avc".equals(name)) + || (Util.SDK_INT <= 17 && "OMX.allwinner.video.decoder.avc".equals(name)) || ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure); } From a3ded59f28e05495446a739aa2f014013859be58 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Jul 2019 13:59:14 +0100 Subject: [PATCH 1457/1556] Check codec profile/level for AV1 Add appropriate unit tests. PiperOrigin-RevId: 258552404 --- .../exoplayer2/mediacodec/MediaCodecUtil.java | 77 ++++++++++++++++++- .../mediacodec/MediaCodecUtilTest.java | 25 ++++++ 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index f3936e5dc2..455ee6c034 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -81,10 +81,13 @@ public final class MediaCodecUtil { private static final Map DOLBY_VISION_STRING_TO_LEVEL; private static final String CODEC_ID_DVHE = "dvhe"; private static final String CODEC_ID_DVH1 = "dvh1"; + // AV1. + private static final SparseIntArray AV1_LEVEL_NUMBER_TO_CONST; + private static final String CODEC_ID_AV01 = "av01"; // MP4A AAC. private static final SparseIntArray MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE; private static final String CODEC_ID_MP4A = "mp4a"; - + // Lazily initialized. private static int maxH264DecodableFrameSize = -1; @@ -239,8 +242,6 @@ public final class MediaCodecUtil { if (codec == null) { return null; } - // TODO: Check codec profile/level for AV1 once targeting Android Q and [Internal: b/128552878] - // has been fixed. String[] parts = codec.split("\\."); switch (parts[0]) { case CODEC_ID_AVC1: @@ -254,6 +255,8 @@ public final class MediaCodecUtil { case CODEC_ID_DVHE: case CODEC_ID_DVH1: return getDolbyVisionProfileAndLevel(codec, parts); + case CODEC_ID_AV01: + return getAv1ProfileAndLevel(codec, parts); case CODEC_ID_MP4A: return getAacCodecProfileAndLevel(codec, parts); default: @@ -684,6 +687,48 @@ public final class MediaCodecUtil { return new Pair<>(profile, level); } + private static Pair getAv1ProfileAndLevel(String codec, String[] parts) { + if (parts.length < 4) { + Log.w(TAG, "Ignoring malformed AV1 codec string: " + codec); + return null; + } + int profileInteger; + int levelInteger; + int bitDepthInteger; + try { + profileInteger = Integer.parseInt(parts[1]); + levelInteger = Integer.parseInt(parts[2].substring(0, 2)); + bitDepthInteger = Integer.parseInt(parts[3]); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed AV1 codec string: " + codec); + return null; + } + + // TODO: Recognize HDR profiles. Currently, the profile is assumed to be either Main8 or Main10. + // See [Internal: b/124435216]. + if (profileInteger != 0) { + Log.w(TAG, "Unknown AV1 profile: " + profileInteger); + return null; + } + if (bitDepthInteger != 8 && bitDepthInteger != 10) { + Log.w(TAG, "Unknown AV1 bit depth: " + bitDepthInteger); + return null; + } + int profile; + if (bitDepthInteger == 8) { + profile = CodecProfileLevel.AV1ProfileMain8; + } else { + profile = CodecProfileLevel.AV1ProfileMain10; + } + + int level = AV1_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1); + if (level == -1) { + Log.w(TAG, "Unknown AV1 level: " + levelInteger); + return null; + } + return new Pair<>(profile, level); + } + /** * Conversion values taken from ISO 14496-10 Table A-1. * @@ -1010,6 +1055,32 @@ public final class MediaCodecUtil { DOLBY_VISION_STRING_TO_LEVEL.put("08", CodecProfileLevel.DolbyVisionLevelUhd48); DOLBY_VISION_STRING_TO_LEVEL.put("09", CodecProfileLevel.DolbyVisionLevelUhd60); + AV1_LEVEL_NUMBER_TO_CONST = new SparseIntArray(); + AV1_LEVEL_NUMBER_TO_CONST.put(0, CodecProfileLevel.AV1Level2); + AV1_LEVEL_NUMBER_TO_CONST.put(1, CodecProfileLevel.AV1Level21); + AV1_LEVEL_NUMBER_TO_CONST.put(2, CodecProfileLevel.AV1Level22); + AV1_LEVEL_NUMBER_TO_CONST.put(3, CodecProfileLevel.AV1Level23); + AV1_LEVEL_NUMBER_TO_CONST.put(4, CodecProfileLevel.AV1Level3); + AV1_LEVEL_NUMBER_TO_CONST.put(5, CodecProfileLevel.AV1Level31); + AV1_LEVEL_NUMBER_TO_CONST.put(6, CodecProfileLevel.AV1Level32); + AV1_LEVEL_NUMBER_TO_CONST.put(7, CodecProfileLevel.AV1Level33); + AV1_LEVEL_NUMBER_TO_CONST.put(8, CodecProfileLevel.AV1Level4); + AV1_LEVEL_NUMBER_TO_CONST.put(9, CodecProfileLevel.AV1Level41); + AV1_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.AV1Level42); + AV1_LEVEL_NUMBER_TO_CONST.put(11, CodecProfileLevel.AV1Level43); + AV1_LEVEL_NUMBER_TO_CONST.put(12, CodecProfileLevel.AV1Level5); + AV1_LEVEL_NUMBER_TO_CONST.put(13, CodecProfileLevel.AV1Level51); + AV1_LEVEL_NUMBER_TO_CONST.put(14, CodecProfileLevel.AV1Level52); + AV1_LEVEL_NUMBER_TO_CONST.put(15, CodecProfileLevel.AV1Level53); + AV1_LEVEL_NUMBER_TO_CONST.put(16, CodecProfileLevel.AV1Level6); + AV1_LEVEL_NUMBER_TO_CONST.put(17, CodecProfileLevel.AV1Level61); + AV1_LEVEL_NUMBER_TO_CONST.put(18, CodecProfileLevel.AV1Level62); + AV1_LEVEL_NUMBER_TO_CONST.put(19, CodecProfileLevel.AV1Level63); + AV1_LEVEL_NUMBER_TO_CONST.put(20, CodecProfileLevel.AV1Level7); + AV1_LEVEL_NUMBER_TO_CONST.put(21, CodecProfileLevel.AV1Level71); + AV1_LEVEL_NUMBER_TO_CONST.put(22, CodecProfileLevel.AV1Level72); + AV1_LEVEL_NUMBER_TO_CONST.put(23, CodecProfileLevel.AV1Level73); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE = new SparseIntArray(); MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(1, CodecProfileLevel.AACObjectMain); MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(2, CodecProfileLevel.AACObjectLC); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java index a84c6f5d7b..05d92e0783 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java @@ -60,6 +60,31 @@ public final class MediaCodecUtilTest { MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelFhd60); } + @Test + public void getCodecProfileAndLevel_handlesAv1ProfileMain8CodecString() { + assertCodecProfileAndLevelForCodecsString( + "av01.0.10M.08", + MediaCodecInfo.CodecProfileLevel.AV1ProfileMain8, + MediaCodecInfo.CodecProfileLevel.AV1Level42); + } + + @Test + public void getCodecProfileAndLevel_handlesAv1ProfileMain10CodecString() { + assertCodecProfileAndLevelForCodecsString( + "av01.0.20M.10", + MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10, + MediaCodecInfo.CodecProfileLevel.AV1Level7); + } + + @Test + public void getCodecProfileAndLevel_handlesFullAv1CodecString() { + // Example from https://aomediacodec.github.io/av1-isobmff/#codecsparam. + assertCodecProfileAndLevelForCodecsString( + "av01.0.04M.10.0.112.09.16.09.0", + MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10, + MediaCodecInfo.CodecProfileLevel.AV1Level3); + } + @Test public void getCodecProfileAndLevel_rejectsNullCodecString() { assertThat(MediaCodecUtil.getCodecProfileAndLevel(/* codec= */ null)).isNull(); From 1a479387f2a4939d298ee8c27d8c98862bd54842 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Jul 2019 16:32:11 +0100 Subject: [PATCH 1458/1556] Fix the equals check PiperOrigin-RevId: 258574110 --- .../google/android/exoplayer2/source/SampleMetadataQueue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index 78b3a35549..89160f45f3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -227,7 +227,7 @@ import com.google.android.exoplayer2.util.Util; return SampleQueue.PEEK_RESULT_NOTHING; } int relativeReadIndex = getRelativeIndex(readPosition); - if (formats[relativeReadIndex].equals(downstreamFormat)) { + if (!formats[relativeReadIndex].equals(downstreamFormat)) { return SampleQueue.PEEK_RESULT_FORMAT; } else { return (flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0 From 049f3cf5cda00fb5acdd9151f6d9040a15e843d9 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 17 Jul 2019 17:33:36 +0100 Subject: [PATCH 1459/1556] Keep default start position (TIME_UNSET) as content position for preroll ads. If we use the default start position, we currently resolve it immediately even if we need to play an ad first, and later try to project forward again if we believe that the default start position should be used. This causes problems if a specific start position is set and the later projection after the preroll ad shouldn't take place. The problem is solved by keeping the content position as TIME_UNSET (= default position) if an ad needs to be played first. The content after the ad can then be resolved to its current default position if needed. PiperOrigin-RevId: 258583948 --- RELEASENOTES.md | 1 + .../android/exoplayer2/ExoPlayerImpl.java | 4 +- .../exoplayer2/ExoPlayerImplInternal.java | 7 ++- .../android/exoplayer2/MediaPeriodInfo.java | 3 +- .../android/exoplayer2/MediaPeriodQueue.java | 21 ++++---- .../android/exoplayer2/PlaybackInfo.java | 3 +- .../android/exoplayer2/ExoPlayerTest.java | 51 +++++++++++++++++++ .../testutil/ExoPlayerTestRunner.java | 2 +- 8 files changed, 77 insertions(+), 15 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9d66a6f800..3382f01e8a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -24,6 +24,7 @@ * Set `compileSdkVersion` to 29 to use Android Q APIs. * Add `enable` and `disable` methods to `MediaSource` to improve resource management in playlists. +* Fix issue where initial seek positions get ignored when playing a preroll ad. ### 2.10.3 ### 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 73107aa98e..f380af968c 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 @@ -493,7 +493,9 @@ import java.util.concurrent.CopyOnWriteArrayList; public long getContentPosition() { if (isPlayingAd()) { playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period); - return period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs); + return playbackInfo.contentPositionUs == C.TIME_UNSET + ? playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDefaultPositionMs() + : period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs); } else { return getCurrentPosition(); } 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 0fc7242279..38cdb57fc8 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 @@ -1303,8 +1303,11 @@ import java.util.concurrent.atomic.AtomicBoolean; Pair defaultPosition = getPeriodPosition( timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); - newContentPositionUs = defaultPosition.second; - newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, newContentPositionUs); + newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, defaultPosition.second); + if (!newPeriodId.isAd()) { + // Keep unset start position if we need to play an ad first. + newContentPositionUs = defaultPosition.second; + } } else if (timeline.getIndexOfPeriod(newPeriodId.periodUid) == C.INDEX_UNSET) { // The current period isn't in the new timeline. Attempt to resolve a subsequent period whose // window we can restart from. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java index bc1ea7b1e1..2733df7ba6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java @@ -29,7 +29,8 @@ import com.google.android.exoplayer2.util.Util; public final long startPositionUs; /** * If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET} - * otherwise. + * if this is not an ad or the next content media period should be played from its default + * position. */ public final long contentPositionUs; /** 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 e47d8af381..0dacd4df30 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 @@ -144,7 +144,9 @@ import com.google.android.exoplayer2.util.Assertions; MediaPeriodInfo info) { long rendererPositionOffsetUs = loading == null - ? (info.id.isAd() ? info.contentPositionUs : 0) + ? (info.id.isAd() && info.contentPositionUs != C.TIME_UNSET + ? info.contentPositionUs + : 0) : (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs); MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder( @@ -560,6 +562,7 @@ import com.google.android.exoplayer2.util.Assertions; } long startPositionUs; + long contentPositionUs; int nextWindowIndex = timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex; Object nextPeriodUid = period.uid; @@ -568,6 +571,7 @@ import com.google.android.exoplayer2.util.Assertions; // We're starting to buffer a new window. When playback transitions to this window we'll // want it to be from its default start position, so project the default start position // forward by the duration of the buffer, and start buffering from this point. + contentPositionUs = C.TIME_UNSET; Pair defaultPosition = timeline.getPeriodPosition( window, @@ -587,12 +591,13 @@ import com.google.android.exoplayer2.util.Assertions; windowSequenceNumber = nextWindowSequenceNumber++; } } else { + // We're starting to buffer a new period within the same window. startPositionUs = 0; + contentPositionUs = 0; } MediaPeriodId periodId = resolveMediaPeriodIdForAds(nextPeriodUid, startPositionUs, windowSequenceNumber); - return getMediaPeriodInfo( - periodId, /* contentPositionUs= */ startPositionUs, startPositionUs); + return getMediaPeriodInfo(periodId, contentPositionUs, startPositionUs); } MediaPeriodId currentPeriodId = mediaPeriodInfo.id; @@ -616,13 +621,11 @@ import com.google.android.exoplayer2.util.Assertions; mediaPeriodInfo.contentPositionUs, currentPeriodId.windowSequenceNumber); } else { - // Play content from the ad group position. As a special case, if we're transitioning from a - // preroll ad group to content and there are no other ad groups, project the start position - // forward as if this were a transition to a new window. No attempt is made to handle - // midrolls in live streams, as it's unclear what content position should play after an ad - // (server-side dynamic ad insertion is more appropriate for this use case). + // Play content from the ad group position. long startPositionUs = mediaPeriodInfo.contentPositionUs; - if (period.getAdGroupCount() == 1 && period.getAdGroupTimeUs(0) == 0) { + if (startPositionUs == C.TIME_UNSET) { + // If we're transitioning from an ad group to content starting from its default position, + // project the start position forward as if this were a transition to a new window. Pair defaultPosition = timeline.getPeriodPosition( window, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index 1eedae08b6..669f41ca13 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -45,7 +45,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /** * If {@link #periodId} refers to an ad, the position of the suspended content relative to the * start of the associated period in the {@link #timeline}, in microseconds. {@link C#TIME_UNSET} - * if {@link #periodId} does not refer to an ad. + * if {@link #periodId} does not refer to an ad or if the suspended content should be played from + * its default position. */ public final long contentPositionUs; /** The current playback state. One of the {@link Player}.STATE_ constants. */ 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 61b8418411..39046b52ce 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 @@ -20,6 +20,7 @@ import static org.junit.Assert.fail; import android.content.Context; import android.graphics.SurfaceTexture; +import android.net.Uri; import androidx.annotation.Nullable; import android.view.Surface; import androidx.test.core.app.ApplicationProvider; @@ -2590,6 +2591,56 @@ public final class ExoPlayerTest { assertThat(bufferedPositionAtFirstDiscontinuityMs.get()).isEqualTo(C.usToMs(windowDurationUs)); } + @Test + public void contentWithInitialSeekPositionAfterPrerollAdStartsAtSeekPosition() throws Exception { + AdPlaybackState adPlaybackState = + FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 3, /* adGroupTimesUs= */ 0) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.parse("https://ad1")) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, Uri.parse("https://ad2")) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, Uri.parse("https://ad3")); + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ 10_000_000, + adPlaybackState)); + final FakeMediaSource fakeMediaSource = new FakeMediaSource(fakeTimeline); + AtomicReference playerReference = new AtomicReference<>(); + AtomicLong contentStartPositionMs = new AtomicLong(C.TIME_UNSET); + EventListener eventListener = + new EventListener() { + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + if (reason == Player.DISCONTINUITY_REASON_AD_INSERTION) { + contentStartPositionMs.set(playerReference.get().getContentPosition()); + } + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("contentWithInitialSeekAfterPrerollAd") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + playerReference.set(player); + player.addListener(eventListener); + } + }) + .seek(5_000) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(fakeMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(contentStartPositionMs.get()).isAtLeast(5_000L); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { 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 f7c6694409..b61c5f9b2c 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 @@ -416,7 +416,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc if (actionSchedule != null) { actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); } - player.prepare(mediaSource); + player.prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); } catch (Exception e) { handleException(e); } From bee35ed9d7aff3d7957d68b291b307054f3e5704 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 17 Jul 2019 18:07:28 +0100 Subject: [PATCH 1460/1556] Fix DeprecationMismatch errors PiperOrigin-RevId: 258590215 --- .../java/com/google/android/exoplayer2/Format.java | 12 ++++++++++++ .../exoplayer2/source/ExtractorMediaSource.java | 5 ++++- .../android/exoplayer2/upstream/HttpDataSource.java | 3 +++ 3 files changed, 19 insertions(+), 1 deletion(-) 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 f06c9da048..df01df1708 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 @@ -179,6 +179,10 @@ public final class Format implements Parcelable { // Video. + /** + * @deprecated Use {@link #createVideoContainerFormat(String, String, String, String, String, int, + * int, int, float, List, int, int)} instead. + */ @Deprecated public static Format createVideoContainerFormat( @Nullable String id, @@ -358,6 +362,10 @@ public final class Format implements Parcelable { // Audio. + /** + * @deprecated Use {@link #createAudioContainerFormat(String, String, String, String, String, int, + * int, int, List, int, int, String)} instead. + */ @Deprecated public static Format createAudioContainerFormat( @Nullable String id, @@ -763,6 +771,10 @@ public final class Format implements Parcelable { // Generic. + /** + * @deprecated Use {@link #createContainerFormat(String, String, String, String, String, int, int, + * int, String)} instead. + */ @Deprecated public static Format createContainerFormat( @Nullable String id, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 9e7da87766..ee731cbc09 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -58,7 +58,7 @@ public final class ExtractorMediaSource extends CompositeMediaSource { } - /** Use {@link ProgressiveMediaSource.Factory} instead. */ + /** @deprecated Use {@link ProgressiveMediaSource.Factory} instead. */ @Deprecated public static final class Factory implements MediaSourceFactory { @@ -221,6 +221,9 @@ public final class ExtractorMediaSource extends CompositeMediaSource { } } + /** + * @deprecated Use {@link ProgressiveMediaSource#DEFAULT_LOADING_CHECK_INTERVAL_BYTES} instead. + */ @Deprecated public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java index 07155ee2bc..17fb4ad7a1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java @@ -183,18 +183,21 @@ public interface HttpDataSource extends DataSource { return defaultRequestProperties; } + /** @deprecated Use {@link #getDefaultRequestProperties} instead. */ @Deprecated @Override public final void setDefaultRequestProperty(String name, String value) { defaultRequestProperties.set(name, value); } + /** @deprecated Use {@link #getDefaultRequestProperties} instead. */ @Deprecated @Override public final void clearDefaultRequestProperty(String name) { defaultRequestProperties.remove(name); } + /** @deprecated Use {@link #getDefaultRequestProperties} instead. */ @Deprecated @Override public final void clearAllDefaultRequestProperties() { From 80d5dabd525727f55a4ad5b74ba67d58b1a9fd32 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 17 Jul 2019 18:22:04 +0100 Subject: [PATCH 1461/1556] Fix DataSchemeDataSource re-opening and range requests Issue:#6192 PiperOrigin-RevId: 258592902 --- RELEASENOTES.md | 2 + .../upstream/DataSchemeDataSource.java | 24 ++++---- .../upstream/DataSchemeDataSourceTest.java | 60 +++++++++++++++++-- 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3382f01e8a..a769ff5f0c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,8 @@ * Add `enable` and `disable` methods to `MediaSource` to improve resource management in playlists. * Fix issue where initial seek positions get ignored when playing a preroll ad. +* Fix `DataSchemeDataSource` re-opening and range requests + ([#6192](https://github.com/google/ExoPlayer/issues/6192)). ### 2.10.3 ### 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 03804fa577..94a6e21c86 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 @@ -33,8 +33,8 @@ public final class DataSchemeDataSource extends BaseDataSource { @Nullable private DataSpec dataSpec; @Nullable private byte[] data; - private int dataLength; - private int bytesRead; + private int endPosition; + private int readPosition; public DataSchemeDataSource() { super(/* isNetwork= */ false); @@ -44,6 +44,7 @@ public final class DataSchemeDataSource extends BaseDataSource { public long open(DataSpec dataSpec) throws IOException { transferInitializing(dataSpec); this.dataSpec = dataSpec; + readPosition = (int) dataSpec.position; Uri uri = dataSpec.uri; String scheme = uri.getScheme(); if (!SCHEME_DATA.equals(scheme)) { @@ -57,17 +58,21 @@ public final class DataSchemeDataSource extends BaseDataSource { if (uriParts[0].contains(";base64")) { try { data = Base64.decode(dataString, 0); - dataLength = data.length; } catch (IllegalArgumentException e) { throw new ParserException("Error while parsing Base64 encoded string: " + dataString, e); } } else { // TODO: Add support for other charsets. data = Util.getUtf8Bytes(URLDecoder.decode(dataString, C.ASCII_NAME)); - dataLength = data.length; + } + endPosition = + dataSpec.length != C.LENGTH_UNSET ? (int) dataSpec.length + readPosition : data.length; + if (endPosition > data.length || readPosition > endPosition) { + data = null; + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); } transferStarted(dataSpec); - return dataLength; + return (long) endPosition - readPosition; } @Override @@ -75,13 +80,13 @@ public final class DataSchemeDataSource extends BaseDataSource { if (readLength == 0) { return 0; } - int remainingBytes = dataLength - bytesRead; + int remainingBytes = endPosition - readPosition; if (remainingBytes == 0) { return C.RESULT_END_OF_INPUT; } readLength = Math.min(readLength, remainingBytes); - System.arraycopy(castNonNull(data), bytesRead, buffer, offset, readLength); - bytesRead += readLength; + System.arraycopy(castNonNull(data), readPosition, buffer, offset, readLength); + readPosition += readLength; bytesTransferred(readLength); return readLength; } @@ -93,12 +98,11 @@ public final class DataSchemeDataSource extends BaseDataSource { } @Override - public void close() throws IOException { + public void close() { if (data != null) { data = null; transferEnded(); } dataSpec = null; } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java index 2df9a608e9..8cb142f05d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.fail; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import org.junit.Before; @@ -31,6 +32,9 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class DataSchemeDataSourceTest { + private static final String DATA_SCHEME_URI = + "data:text/plain;base64,eyJwcm92aWRlciI6IndpZGV2aW5lX3Rlc3QiLCJjb250ZW50X2lkIjoiTWpBeE5WOTBaV" + + "0Z5Y3c9PSIsImtleV9pZHMiOlsiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiXX0="; private DataSource schemeDataDataSource; @Before @@ -40,9 +44,7 @@ public final class DataSchemeDataSourceTest { @Test public void testBase64Data() throws IOException { - DataSpec dataSpec = buildDataSpec("data:text/plain;base64,eyJwcm92aWRlciI6IndpZGV2aW5lX3Rlc3QiL" - + "CJjb250ZW50X2lkIjoiTWpBeE5WOTBaV0Z5Y3c9PSIsImtleV9pZHMiOlsiMDAwMDAwMDAwMDAwMDAwMDAwMDAwM" - + "DAwMDAwMDAwMDAiXX0="); + DataSpec dataSpec = buildDataSpec(DATA_SCHEME_URI); DataSourceAsserts.assertDataSourceContent( schemeDataDataSource, dataSpec, @@ -72,6 +74,52 @@ public final class DataSchemeDataSourceTest { assertThat(Util.fromUtf8Bytes(buffer, 0, 18)).isEqualTo("012345678901234567"); } + @Test + public void testSequentialRangeRequests() throws IOException { + DataSpec dataSpec = + buildDataSpec(DATA_SCHEME_URI, /* position= */ 1, /* length= */ C.LENGTH_UNSET); + DataSourceAsserts.assertDataSourceContent( + schemeDataDataSource, + dataSpec, + Util.getUtf8Bytes( + "\"provider\":\"widevine_test\",\"content_id\":\"MjAxNV90ZWFycw==\",\"key_ids\":" + + "[\"00000000000000000000000000000000\"]}")); + dataSpec = buildDataSpec(DATA_SCHEME_URI, /* position= */ 10, /* length= */ C.LENGTH_UNSET); + DataSourceAsserts.assertDataSourceContent( + schemeDataDataSource, + dataSpec, + Util.getUtf8Bytes( + "\":\"widevine_test\",\"content_id\":\"MjAxNV90ZWFycw==\",\"key_ids\":" + + "[\"00000000000000000000000000000000\"]}")); + dataSpec = buildDataSpec(DATA_SCHEME_URI, /* position= */ 15, /* length= */ 5); + DataSourceAsserts.assertDataSourceContent( + schemeDataDataSource, dataSpec, Util.getUtf8Bytes("devin")); + } + + @Test + public void testInvalidStartPositionRequest() throws IOException { + try { + // Try to open a range starting one byte beyond the resource's length. + schemeDataDataSource.open( + buildDataSpec(DATA_SCHEME_URI, /* position= */ 108, /* length= */ C.LENGTH_UNSET)); + fail(); + } catch (DataSourceException e) { + assertThat(e.reason).isEqualTo(DataSourceException.POSITION_OUT_OF_RANGE); + } + } + + @Test + public void testRangeExceedingResourceLengthRequest() throws IOException { + try { + // Try to open a range exceeding the resource's length. + schemeDataDataSource.open( + buildDataSpec(DATA_SCHEME_URI, /* position= */ 97, /* length= */ 11)); + fail(); + } catch (DataSourceException e) { + assertThat(e.reason).isEqualTo(DataSourceException.POSITION_OUT_OF_RANGE); + } + } + @Test public void testIncorrectScheme() { try { @@ -99,7 +147,11 @@ public final class DataSchemeDataSourceTest { } private static DataSpec buildDataSpec(String uriString) { - return new DataSpec(Uri.parse(uriString)); + return buildDataSpec(uriString, /* position= */ 0, /* length= */ C.LENGTH_UNSET); + } + + private static DataSpec buildDataSpec(String uriString, int position, int length) { + return new DataSpec(Uri.parse(uriString), position, length, /* key= */ null); } } From c779e84cbb911b33357a909624804a5870b99340 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 18 Jul 2019 10:08:19 +0100 Subject: [PATCH 1462/1556] Switch language normalization to 2-letter language codes. 2-letter codes (ISO 639-1) are the standard Android normalization and thus we should prefer them to 3-letter codes (although both are technically allowed according the BCP47). This helps in two ways: 1. It simplifies app interaction with our normalized language codes as the Locale class makes it easy to convert a 2-letter to a 3-letter code but not the other way round. 2. It better normalizes codes on API<21 where we previously had issues with language+country codes (see tests). 3. It allows us to normalize both ISO 639-2/T and ISO 639-2/B codes to the same language. PiperOrigin-RevId: 258729728 --- RELEASENOTES.md | 2 + .../trackselection/DefaultTrackSelector.java | 10 +-- .../google/android/exoplayer2/util/Util.java | 80 ++++++++++++++++--- .../android/exoplayer2/util/UtilTest.java | 46 ++++++++--- .../playlist/HlsMasterPlaylistParserTest.java | 2 +- 5 files changed, 114 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a769ff5f0c..7194882758 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,6 +27,8 @@ * Fix issue where initial seek positions get ignored when playing a preroll ad. * Fix `DataSchemeDataSource` re-opening and range requests ([#6192](https://github.com/google/ExoPlayer/issues/6192)). +* Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language + tags instead of 3-letter ISO 639-2 language tags. ### 2.10.3 ### 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 949bd178ea..b8dd40f8bd 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 @@ -2318,14 +2318,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (TextUtils.equals(format.language, language)) { return 3; } - // Partial match where one language is a subset of the other (e.g. "zho-hans" and "zho-hans-hk") + // Partial match where one language is a subset of the other (e.g. "zh-hans" and "zh-hans-hk") if (format.language.startsWith(language) || language.startsWith(format.language)) { return 2; } - // Partial match where only the main language tag is the same (e.g. "fra-fr" and "fra-ca") - if (format.language.length() >= 3 - && language.length() >= 3 - && format.language.substring(0, 3).equals(language.substring(0, 3))) { + // Partial match where only the main language tag is the same (e.g. "fr-fr" and "fr-ca") + String formatMainLanguage = Util.splitAtFirst(format.language, "-")[0]; + String queryMainLanguage = Util.splitAtFirst(language, "-")[0]; + if (formatMainLanguage.equals(queryMainLanguage)) { return 1; } return 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 32e9c32a53..a8aa2c630b 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 @@ -71,6 +71,7 @@ import java.util.Calendar; import java.util.Collections; import java.util.Formatter; import java.util.GregorianCalendar; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.MissingResourceException; @@ -135,6 +136,10 @@ public final class Util { + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})"); + // Android standardizes to ISO 639-1 2-letter codes and provides no way to map a 3-letter + // ISO 639-2 code back to the corresponding 2-letter code. + @Nullable private static HashMap languageTagIso3ToIso2; + private Util() {} /** @@ -465,18 +470,25 @@ public final class Util { if (language == null) { return null; } - try { - Locale locale = getLocaleForLanguageTag(language); - int localeLanguageLength = locale.getLanguage().length(); - String normLanguage = locale.getISO3Language(); - if (normLanguage.isEmpty()) { - return toLowerInvariant(language); - } - String normTag = getLocaleLanguageTag(locale); - return toLowerInvariant(normLanguage + normTag.substring(localeLanguageLength)); - } catch (MissingResourceException e) { + Locale locale = getLocaleForLanguageTag(language); + String localeLanguage = locale.getLanguage(); + int localeLanguageLength = localeLanguage.length(); + if (localeLanguageLength == 0) { + // Return original language for invalid language tags. return toLowerInvariant(language); + } else if (localeLanguageLength == 3) { + // Locale.toLanguageTag will ensure a normalized well-formed output. However, 3-letter + // ISO 639-2 language codes will not be converted to 2-letter ISO 639-1 codes automatically. + if (languageTagIso3ToIso2 == null) { + languageTagIso3ToIso2 = createIso3ToIso2Map(); + } + String iso2Language = languageTagIso3ToIso2.get(localeLanguage); + if (iso2Language != null) { + localeLanguage = iso2Language; + } } + String normTag = getLocaleLanguageTag(locale); + return toLowerInvariant(localeLanguage + normTag.substring(localeLanguageLength)); } /** @@ -2028,6 +2040,54 @@ public final class Util { } } + private static HashMap createIso3ToIso2Map() { + String[] iso2Languages = Locale.getISOLanguages(); + HashMap iso3ToIso2 = + new HashMap<>( + /* initialCapacity= */ iso2Languages.length + iso3BibliographicalToIso2.length); + for (String iso2 : iso2Languages) { + try { + // This returns the ISO 639-2/T code for the language. + String iso3 = new Locale(iso2).getISO3Language(); + if (!TextUtils.isEmpty(iso3)) { + iso3ToIso2.put(iso3, iso2); + } + } catch (MissingResourceException e) { + // Shouldn't happen for list of known languages, but we don't want to throw either. + } + } + // Add additional ISO 639-2/B codes to mapping. + for (int i = 0; i < iso3BibliographicalToIso2.length; i += 2) { + iso3ToIso2.put(iso3BibliographicalToIso2[i], iso3BibliographicalToIso2[i + 1]); + } + return iso3ToIso2; + } + + // See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes. + private static final String[] iso3BibliographicalToIso2 = + new String[] { + "alb", "sq", + "arm", "hy", + "baq", "eu", + "bur", "my", + "tib", "bo", + "chi", "zh", + "cze", "cs", + "dut", "nl", + "ger", "de", + "gre", "el", + "fre", "fr", + "geo", "ka", + "ice", "is", + "mac", "mk", + "mao", "mi", + "may", "ms", + "per", "fa", + "rum", "ro", + "slo", "sk", + "wel", "cy" + }; + /** * Allows the CRC calculation to be done byte by byte instead of bit per bit being the order * "most significant bit first". diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index 9abec0cd8f..f85ee37c07 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -268,14 +268,15 @@ public class UtilTest { @Test @Config(sdk = 21) public void testNormalizeLanguageCodeV21() { - assertThat(Util.normalizeLanguageCode("es")).isEqualTo("spa"); - assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("spa"); - assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("spa-ar"); - assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("spa-ar"); - assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("spa-ar-dialect"); - assertThat(Util.normalizeLanguageCode("es-419")).isEqualTo("spa-419"); - assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zho-hans-tw"); - assertThat(Util.normalizeLanguageCode("zh-tw-hans")).isEqualTo("zho-tw"); + assertThat(Util.normalizeLanguageCode("es")).isEqualTo("es"); + assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); + assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("es-ar-dialect"); + assertThat(Util.normalizeLanguageCode("ES-419")).isEqualTo("es-419"); + assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zh-hans-tw"); + assertThat(Util.normalizeLanguageCode("zh-tw-hans")).isEqualTo("zh-tw"); + assertThat(Util.normalizeLanguageCode("zho-hans-tw")).isEqualTo("zh-hans-tw"); assertThat(Util.normalizeLanguageCode("und")).isEqualTo("und"); assertThat(Util.normalizeLanguageCode("DoesNotExist")).isEqualTo("doesnotexist"); } @@ -283,13 +284,38 @@ public class UtilTest { @Test @Config(sdk = 16) public void testNormalizeLanguageCode() { - assertThat(Util.normalizeLanguageCode("es")).isEqualTo("spa"); - assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("spa"); + assertThat(Util.normalizeLanguageCode("es")).isEqualTo("es"); + assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); assertThat(Util.normalizeLanguageCode("und")).isEqualTo("und"); assertThat(Util.normalizeLanguageCode("DoesNotExist")).isEqualTo("doesnotexist"); } + @Test + public void testNormalizeIso6392BibliographicalAndTextualCodes() { + // See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes. + assertThat(Util.normalizeLanguageCode("alb")).isEqualTo(Util.normalizeLanguageCode("sqi")); + assertThat(Util.normalizeLanguageCode("arm")).isEqualTo(Util.normalizeLanguageCode("hye")); + assertThat(Util.normalizeLanguageCode("baq")).isEqualTo(Util.normalizeLanguageCode("eus")); + assertThat(Util.normalizeLanguageCode("bur")).isEqualTo(Util.normalizeLanguageCode("mya")); + assertThat(Util.normalizeLanguageCode("chi")).isEqualTo(Util.normalizeLanguageCode("zho")); + assertThat(Util.normalizeLanguageCode("cze")).isEqualTo(Util.normalizeLanguageCode("ces")); + assertThat(Util.normalizeLanguageCode("dut")).isEqualTo(Util.normalizeLanguageCode("nld")); + assertThat(Util.normalizeLanguageCode("fre")).isEqualTo(Util.normalizeLanguageCode("fra")); + assertThat(Util.normalizeLanguageCode("geo")).isEqualTo(Util.normalizeLanguageCode("kat")); + assertThat(Util.normalizeLanguageCode("ger")).isEqualTo(Util.normalizeLanguageCode("deu")); + assertThat(Util.normalizeLanguageCode("gre")).isEqualTo(Util.normalizeLanguageCode("ell")); + assertThat(Util.normalizeLanguageCode("ice")).isEqualTo(Util.normalizeLanguageCode("isl")); + assertThat(Util.normalizeLanguageCode("mac")).isEqualTo(Util.normalizeLanguageCode("mkd")); + assertThat(Util.normalizeLanguageCode("mao")).isEqualTo(Util.normalizeLanguageCode("mri")); + assertThat(Util.normalizeLanguageCode("may")).isEqualTo(Util.normalizeLanguageCode("msa")); + assertThat(Util.normalizeLanguageCode("per")).isEqualTo(Util.normalizeLanguageCode("fas")); + assertThat(Util.normalizeLanguageCode("rum")).isEqualTo(Util.normalizeLanguageCode("ron")); + assertThat(Util.normalizeLanguageCode("slo")).isEqualTo(Util.normalizeLanguageCode("slk")); + assertThat(Util.normalizeLanguageCode("tib")).isEqualTo(Util.normalizeLanguageCode("bod")); + assertThat(Util.normalizeLanguageCode("wel")).isEqualTo(Util.normalizeLanguageCode("cym")); + } + private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) { assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName); assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index 095739271e..254a2b2bd1 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -263,7 +263,7 @@ public class HlsMasterPlaylistParserTest { Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0); assertThat(closedCaptionFormat.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_CEA708); assertThat(closedCaptionFormat.accessibilityChannel).isEqualTo(4); - assertThat(closedCaptionFormat.language).isEqualTo("spa"); + assertThat(closedCaptionFormat.language).isEqualTo("es"); } @Test From e25340be3d424f8cdba2531984e2db6624382392 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Jul 2019 12:40:40 +0100 Subject: [PATCH 1463/1556] Pass format instead of codec string when getting profile and level AV1 profile recognition requires additional info contained in format. PiperOrigin-RevId: 258746315 --- .../exoplayer2/mediacodec/MediaCodecInfo.java | 29 ++++++------ .../exoplayer2/mediacodec/MediaCodecUtil.java | 29 ++++++------ .../video/MediaCodecVideoRenderer.java | 6 +-- .../mediacodec/MediaCodecUtilTest.java | 44 +++++++++++++++++-- 4 files changed, 71 insertions(+), 37 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index 3310b0dc8b..acaf798b41 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -198,7 +198,7 @@ public final class MediaCodecInfo { * @throws MediaCodecUtil.DecoderQueryException Thrown if an error occurs while querying decoders. */ public boolean isFormatSupported(Format format) throws MediaCodecUtil.DecoderQueryException { - if (!isCodecSupported(format.codecs)) { + if (!isCodecSupported(format)) { return false; } @@ -226,25 +226,25 @@ public final class MediaCodecInfo { } /** - * Whether the decoder supports the given {@code codec}. If there is insufficient information to - * decide, returns true. + * Whether the decoder supports the codec of the given {@code format}. If there is insufficient + * information to decide, returns true. * - * @param codec Codec string as defined in RFC 6381. - * @return True if the given codec is supported by the decoder. + * @param format The input media format. + * @return True if the codec of the given {@code format} is supported by the decoder. */ - public boolean isCodecSupported(String codec) { - if (codec == null || mimeType == null) { + public boolean isCodecSupported(Format format) { + if (format.codecs == null || mimeType == null) { return true; } - String codecMimeType = MimeTypes.getMediaMimeType(codec); + String codecMimeType = MimeTypes.getMediaMimeType(format.codecs); if (codecMimeType == null) { return true; } if (!mimeType.equals(codecMimeType)) { - logNoSupport("codec.mime " + codec + ", " + codecMimeType); + logNoSupport("codec.mime " + format.codecs + ", " + codecMimeType); return false; } - Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(codec); + Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); if (codecProfileAndLevel == null) { // If we don't know any better, we assume that the profile and level are supported. return true; @@ -261,7 +261,7 @@ public final class MediaCodecInfo { return true; } } - logNoSupport("codec.profileLevel, " + codec + ", " + codecMimeType); + logNoSupport("codec.profileLevel, " + format.codecs + ", " + codecMimeType); return false; } @@ -279,8 +279,7 @@ public final class MediaCodecInfo { if (isVideo) { return adaptive; } else { - Pair codecProfileLevel = - MediaCodecUtil.getCodecProfileAndLevel(format.codecs); + Pair codecProfileLevel = MediaCodecUtil.getCodecProfileAndLevel(format); return codecProfileLevel != null && codecProfileLevel.first == CodecProfileLevel.AACObjectXHE; } } @@ -314,9 +313,9 @@ public final class MediaCodecInfo { } // Check the codec profile levels support adaptation. Pair oldCodecProfileLevel = - MediaCodecUtil.getCodecProfileAndLevel(oldFormat.codecs); + MediaCodecUtil.getCodecProfileAndLevel(oldFormat); Pair newCodecProfileLevel = - MediaCodecUtil.getCodecProfileAndLevel(newFormat.codecs); + MediaCodecUtil.getCodecProfileAndLevel(newFormat); if (oldCodecProfileLevel == null || newCodecProfileLevel == null) { return false; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 455ee6c034..df5ca05972 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -230,35 +230,34 @@ public final class MediaCodecUtil { } /** - * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the given - * codec description string (as defined by RFC 6381). + * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the codec + * description string (as defined by RFC 6381) of the given format. * - * @param codec A codec description string, as defined by RFC 6381, or {@code null} if not known. - * @return A pair (profile constant, level constant) if {@code codec} is well-formed and - * recognized, or null otherwise + * @param format Media format with a codec description string, as defined by RFC 6381. + * @return A pair (profile constant, level constant) if the codec of the {@code format} is + * well-formed and recognized, or null otherwise. */ - @Nullable - public static Pair getCodecProfileAndLevel(@Nullable String codec) { - if (codec == null) { + public static Pair getCodecProfileAndLevel(Format format) { + if (format.codecs == null) { return null; } - String[] parts = codec.split("\\."); + String[] parts = format.codecs.split("\\."); switch (parts[0]) { case CODEC_ID_AVC1: case CODEC_ID_AVC2: - return getAvcProfileAndLevel(codec, parts); + return getAvcProfileAndLevel(format.codecs, parts); case CODEC_ID_VP09: - return getVp9ProfileAndLevel(codec, parts); + return getVp9ProfileAndLevel(format.codecs, parts); case CODEC_ID_HEV1: case CODEC_ID_HVC1: - return getHevcProfileAndLevel(codec, parts); + return getHevcProfileAndLevel(format.codecs, parts); case CODEC_ID_DVHE: case CODEC_ID_DVH1: - return getDolbyVisionProfileAndLevel(codec, parts); + return getDolbyVisionProfileAndLevel(format.codecs, parts); case CODEC_ID_AV01: - return getAv1ProfileAndLevel(codec, parts); + return getAv1ProfileAndLevel(format.codecs, parts); case CODEC_ID_MP4A: - return getAacCodecProfileAndLevel(codec, parts); + return getAacCodecProfileAndLevel(format.codecs, parts); default: return null; } 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 d9d81cf6d4..6e3114d1b1 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 @@ -390,8 +390,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format); if (MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) { // Fallback to primary decoders for H.265/HEVC or H.264/AVC for the relevant DV profiles. - Pair codecProfileAndLevel = - MediaCodecUtil.getCodecProfileAndLevel(format.codecs); + Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); if (codecProfileAndLevel != null) { int profile = codecProfileAndLevel.first; if (profile == 4 || profile == 8) { @@ -1194,8 +1193,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) { // Some phones require the profile to be set on the codec. // See https://github.com/google/ExoPlayer/pull/5438. - Pair codecProfileAndLevel = - MediaCodecUtil.getCodecProfileAndLevel(format.codecs); + Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); if (codecProfileAndLevel != null) { MediaFormatUtil.maybeSetInteger( mediaFormat, MediaFormat.KEY_PROFILE, codecProfileAndLevel.first); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java index 05d92e0783..c485ff49f6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java @@ -20,6 +20,8 @@ import static com.google.common.truth.Truth.assertThat; import android.media.MediaCodecInfo; import android.util.Pair; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.MimeTypes; import org.junit.Test; import org.junit.runner.RunWith; @@ -87,17 +89,53 @@ public final class MediaCodecUtilTest { @Test public void getCodecProfileAndLevel_rejectsNullCodecString() { - assertThat(MediaCodecUtil.getCodecProfileAndLevel(/* codec= */ null)).isNull(); + Format format = + Format.createVideoSampleFormat( + /* id= */ null, + /* sampleMimeType= */ MimeTypes.VIDEO_UNKNOWN, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + /* width= */ 1024, + /* height= */ 768, + /* frameRate= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null); + assertThat(MediaCodecUtil.getCodecProfileAndLevel(format)).isNull(); } @Test public void getCodecProfileAndLevel_rejectsEmptyCodecString() { - assertThat(MediaCodecUtil.getCodecProfileAndLevel("")).isNull(); + Format format = + Format.createVideoSampleFormat( + /* id= */ null, + /* sampleMimeType= */ MimeTypes.VIDEO_UNKNOWN, + /* codecs= */ "", + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + /* width= */ 1024, + /* height= */ 768, + /* frameRate= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null); + assertThat(MediaCodecUtil.getCodecProfileAndLevel(format)).isNull(); } private static void assertCodecProfileAndLevelForCodecsString( String codecs, int profile, int level) { - Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(codecs); + Format format = + Format.createVideoSampleFormat( + /* id= */ null, + /* sampleMimeType= */ MimeTypes.VIDEO_UNKNOWN, + /* codecs= */ codecs, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + /* width= */ 1024, + /* height= */ 768, + /* frameRate= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null); + Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); assertThat(codecProfileAndLevel).isNotNull(); assertThat(codecProfileAndLevel.first).isEqualTo(profile); assertThat(codecProfileAndLevel.second).isEqualTo(level); From c67f18764f038f8a39a1c6aaacea5458f136427d Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Jul 2019 13:45:00 +0100 Subject: [PATCH 1464/1556] Move Format equality check back to write side of sample queue PiperOrigin-RevId: 258752996 --- .../source/SampleMetadataQueue.java | 19 +++++++++--- .../exoplayer2/source/SampleQueueTest.java | 31 +++++++++++++++++-- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index 89160f45f3..09bc438f90 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -62,6 +62,7 @@ import com.google.android.exoplayer2.util.Util; private boolean upstreamKeyframeRequired; private boolean upstreamFormatRequired; private Format upstreamFormat; + private Format upstreamCommittedFormat; private int upstreamSourceId; public SampleMetadataQueue() { @@ -96,6 +97,7 @@ import com.google.android.exoplayer2.util.Util; largestDiscardedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE; isLastSampleQueued = false; + upstreamCommittedFormat = null; if (resetUpstreamFormat) { upstreamFormat = null; upstreamFormatRequired = true; @@ -227,7 +229,7 @@ import com.google.android.exoplayer2.util.Util; return SampleQueue.PEEK_RESULT_NOTHING; } int relativeReadIndex = getRelativeIndex(readPosition); - if (!formats[relativeReadIndex].equals(downstreamFormat)) { + if (formats[relativeReadIndex] != downstreamFormat) { return SampleQueue.PEEK_RESULT_FORMAT; } else { return (flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0 @@ -274,8 +276,7 @@ import com.google.android.exoplayer2.util.Util; if (loadingFinished || isLastSampleQueued) { buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); return C.RESULT_BUFFER_READ; - } else if (upstreamFormat != null - && (formatRequired || !upstreamFormat.equals(downstreamFormat))) { + } else if (upstreamFormat != null && (formatRequired || upstreamFormat != downstreamFormat)) { formatHolder.format = upstreamFormat; return C.RESULT_FORMAT_READ; } else { @@ -284,7 +285,7 @@ import com.google.android.exoplayer2.util.Util; } int relativeReadIndex = getRelativeIndex(readPosition); - if (formatRequired || !formats[relativeReadIndex].equals(downstreamFormat)) { + if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { formatHolder.format = formats[relativeReadIndex]; return C.RESULT_FORMAT_READ; } @@ -422,7 +423,16 @@ import com.google.android.exoplayer2.util.Util; } upstreamFormatRequired = false; if (Util.areEqual(format, upstreamFormat)) { + // The format is unchanged. If format and upstreamFormat are different objects, we keep the + // current upstreamFormat so we can detect format changes in read() using cheap referential + // equality. return false; + } else if (Util.areEqual(format, upstreamCommittedFormat)) { + // The format has changed back to the format of the last committed sample. If they are + // different objects, we revert back to using upstreamCommittedFormat as the upstreamFormat so + // we can detect format changes in read() using cheap referential equality. + upstreamFormat = upstreamCommittedFormat; + return true; } else { upstreamFormat = format; return true; @@ -450,6 +460,7 @@ import com.google.android.exoplayer2.util.Util; cryptoDatas[relativeEndIndex] = cryptoData; formats[relativeEndIndex] = upstreamFormat; sourceIds[relativeEndIndex] = upstreamSourceId; + upstreamCommittedFormat = upstreamFormat; length++; if (length == capacity) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java index bfc6bb52c9..6812e08ef7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -129,10 +129,10 @@ public final class SampleQueueTest { } @Test - public void testReadFormatDeduplicated() { + public void testEqualFormatsDeduplicated() { sampleQueue.format(FORMAT_1); assertReadFormat(false, FORMAT_1); - // If the same format is input then it should be de-duplicated (i.e. not output again). + // If the same format is written then it should not cause a format change on the read side. sampleQueue.format(FORMAT_1); assertNoSamplesToRead(FORMAT_1); // The same applies for a format that's equal (but a different object). @@ -140,6 +140,33 @@ public final class SampleQueueTest { assertNoSamplesToRead(FORMAT_1); } + @Test + public void testMultipleFormatsDeduplicated() { + sampleQueue.format(FORMAT_1); + sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE); + sampleQueue.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); + // Writing multiple formats should not cause a format change on the read side, provided the last + // format to be written is equal to the format of the previous sample. + sampleQueue.format(FORMAT_2); + sampleQueue.format(FORMAT_1_COPY); + sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE); + sampleQueue.sampleMetadata(1000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); + + assertReadFormat(false, FORMAT_1); + assertReadSample(0, true, DATA, 0, ALLOCATION_SIZE); + // Assert the second sample is read without a format change. + assertReadSample(1000, true, DATA, 0, ALLOCATION_SIZE); + + // The same applies if the queue is empty when the formats are written. + sampleQueue.format(FORMAT_2); + sampleQueue.format(FORMAT_1); + sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE); + sampleQueue.sampleMetadata(2000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); + + // Assert the third sample is read without a format change. + assertReadSample(2000, true, DATA, 0, ALLOCATION_SIZE); + } + @Test public void testReadSingleSamples() { sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE); From e4f849076c01b2327a9083bfa5d62f0d877149e3 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 18 Jul 2019 14:00:18 +0100 Subject: [PATCH 1465/1556] Remove unused extractor constructors PiperOrigin-RevId: 258754710 --- .../extractor/DefaultExtractorsFactory.java | 3 +-- .../exoplayer2/extractor/ts/Ac3Extractor.java | 9 ++------- .../exoplayer2/extractor/ts/Ac4Extractor.java | 9 +-------- .../exoplayer2/extractor/ts/AdtsExtractor.java | 18 ++++++------------ .../extractor/ts/AdtsExtractorSeekTest.java | 4 +--- .../extractor/ts/AdtsExtractorTest.java | 5 +---- 6 files changed, 12 insertions(+), 36 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index 54c78eb33d..02c676dfdf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -108,7 +108,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { /** * Sets flags for {@link AdtsExtractor} instances created by the factory. * - * @see AdtsExtractor#AdtsExtractor(long, int) + * @see AdtsExtractor#AdtsExtractor(int) * @param flags The flags to use. * @return The factory, for convenience. */ @@ -220,7 +220,6 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { : 0)); extractors[4] = new AdtsExtractor( - /* firstStreamSampleTimestampUs= */ 0, adtsFlags | (constantBitrateSeekingEnabled ? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index 0a0755327c..2f46744ea0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -46,18 +46,13 @@ public final class Ac3Extractor implements Extractor { private static final int MAX_SYNC_FRAME_SIZE = 2786; private static final int ID3_TAG = 0x00494433; - private final long firstSampleTimestampUs; private final Ac3Reader reader; private final ParsableByteArray sampleData; private boolean startedPacket; + /** Creates a new extractor for AC-3 bitstreams. */ public Ac3Extractor() { - this(0); - } - - public Ac3Extractor(long firstSampleTimestampUs) { - this.firstSampleTimestampUs = firstSampleTimestampUs; reader = new Ac3Reader(); sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE); } @@ -141,7 +136,7 @@ public final class Ac3Extractor implements Extractor { if (!startedPacket) { // Pass data to the reader as though it's contained within a single infinitely long packet. - reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR); + reader.packetStarted(/* pesTimeUs= */ 0, FLAG_DATA_ALIGNMENT_INDICATOR); startedPacket = true; } // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java index 4db02e0d83..2e8dcd952b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java @@ -54,7 +54,6 @@ public final class Ac4Extractor implements Extractor { private static final int ID3_TAG = 0x00494433; - private final long firstSampleTimestampUs; private final Ac4Reader reader; private final ParsableByteArray sampleData; @@ -62,12 +61,6 @@ public final class Ac4Extractor implements Extractor { /** Creates a new extractor for AC-4 bitstreams. */ public Ac4Extractor() { - this(/* firstSampleTimestampUs= */ 0); - } - - /** Creates a new extractor for AC-4 bitstreams, using the specified first sample timestamp. */ - public Ac4Extractor(long firstSampleTimestampUs) { - this.firstSampleTimestampUs = firstSampleTimestampUs; reader = new Ac4Reader(); sampleData = new ParsableByteArray(READ_BUFFER_SIZE); } @@ -152,7 +145,7 @@ public final class Ac4Extractor implements Extractor { if (!startedPacket) { // Pass data to the reader as though it's contained within a single infinitely long packet. - reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR); + reader.packetStarted(/* pesTimeUs= */ 0, FLAG_DATA_ALIGNMENT_INDICATOR); startedPacket = true; } // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index d1e3217e30..3dd88fbb51 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -83,7 +83,6 @@ public final class AdtsExtractor implements Extractor { private final ParsableByteArray packetBuffer; private final ParsableByteArray scratch; private final ParsableBitArray scratchBits; - private final long firstStreamSampleTimestampUs; @Nullable private ExtractorOutput extractorOutput; @@ -94,22 +93,17 @@ public final class AdtsExtractor implements Extractor { private boolean startedPacket; private boolean hasOutputSeekMap; + /** Creates a new extractor for ADTS bitstreams. */ public AdtsExtractor() { - this(0); - } - - public AdtsExtractor(long firstStreamSampleTimestampUs) { - this(/* firstStreamSampleTimestampUs= */ firstStreamSampleTimestampUs, /* flags= */ 0); + this(/* flags= */ 0); } /** - * @param firstStreamSampleTimestampUs The timestamp to be used for the first sample of the stream - * output from this extractor. + * Creates a new extractor for ADTS bitstreams. + * * @param flags Flags that control the extractor's behavior. */ - public AdtsExtractor(long firstStreamSampleTimestampUs, @Flags int flags) { - this.firstStreamSampleTimestampUs = firstStreamSampleTimestampUs; - this.firstSampleTimestampUs = firstStreamSampleTimestampUs; + public AdtsExtractor(@Flags int flags) { this.flags = flags; reader = new AdtsReader(true); packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); @@ -172,7 +166,7 @@ public final class AdtsExtractor implements Extractor { public void seek(long position, long timeUs) { startedPacket = false; reader.seek(); - firstSampleTimestampUs = firstStreamSampleTimestampUs + timeUs; + firstSampleTimestampUs = timeUs; } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java index 4527e41f34..060f7fb81d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java @@ -217,9 +217,7 @@ public final class AdtsExtractorSeekTest { // Internal methods private static AdtsExtractor createAdtsExtractor() { - return new AdtsExtractor( - /* firstStreamSampleTimestampUs= */ 0, - /* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING); + return new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING); } private void assertFirstSampleAfterSeekContainTargetSeekTime( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java index 25e2a336ff..feb14d1adb 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java @@ -32,10 +32,7 @@ public final class AdtsExtractorTest { @Test public void testSample_withSeeking() throws Exception { ExtractorAsserts.assertBehavior( - () -> - new AdtsExtractor( - /* firstStreamSampleTimestampUs= */ 0, - /* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING), + () -> new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING), "ts/sample_cbs.adts"); } } From aeb2fefe483323ed07ab1e0881ae351fedda13c9 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 18 Jul 2019 16:18:49 +0100 Subject: [PATCH 1466/1556] Further language normalization tweaks for API < 21. 1. Using the Locale on API<21 doesn't make any sense because it's a no-op anyway. Slightly restructured the code to avoid that. 2. API<21 often reports languages with non-standard underscores instead of dashes. Normalize that too. 3. Some invalid language tags on API>21 get normalized to "und". Use original tag in such a case. Issue:#6153 PiperOrigin-RevId: 258773463 --- RELEASENOTES.md | 3 + .../google/android/exoplayer2/util/Util.java | 57 +++++++++---------- .../android/exoplayer2/util/UtilTest.java | 15 +++++ 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7194882758..30098e01df 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,9 @@ ([#6192](https://github.com/google/ExoPlayer/issues/6192)). * Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language tags instead of 3-letter ISO 639-2 language tags. +* Fix issue where invalid language tags were normalized to "und" instead of + keeping the original + ([#6153](https://github.com/google/ExoPlayer/issues/6153)). ### 2.10.3 ### 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 a8aa2c630b..e700fc6751 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 @@ -470,25 +470,31 @@ public final class Util { if (language == null) { return null; } - Locale locale = getLocaleForLanguageTag(language); - String localeLanguage = locale.getLanguage(); - int localeLanguageLength = localeLanguage.length(); - if (localeLanguageLength == 0) { - // Return original language for invalid language tags. - return toLowerInvariant(language); - } else if (localeLanguageLength == 3) { - // Locale.toLanguageTag will ensure a normalized well-formed output. However, 3-letter - // ISO 639-2 language codes will not be converted to 2-letter ISO 639-1 codes automatically. + // Locale data (especially for API < 21) may produce tags with '_' instead of the + // standard-conformant '-'. + String normalizedTag = language.replace('_', '-'); + if (Util.SDK_INT >= 21) { + // Filters out ill-formed sub-tags, replaces deprecated tags and normalizes all valid tags. + normalizedTag = normalizeLanguageCodeSyntaxV21(normalizedTag); + } + if (normalizedTag.isEmpty() || "und".equals(normalizedTag)) { + // Tag isn't valid, keep using the original. + normalizedTag = language; + } + normalizedTag = Util.toLowerInvariant(normalizedTag); + String mainLanguage = Util.splitAtFirst(normalizedTag, "-")[0]; + if (mainLanguage.length() == 3) { + // 3-letter ISO 639-2/B or ISO 639-2/T language codes will not be converted to 2-letter ISO + // 639-1 codes automatically. if (languageTagIso3ToIso2 == null) { languageTagIso3ToIso2 = createIso3ToIso2Map(); } - String iso2Language = languageTagIso3ToIso2.get(localeLanguage); + String iso2Language = languageTagIso3ToIso2.get(mainLanguage); if (iso2Language != null) { - localeLanguage = iso2Language; + normalizedTag = iso2Language + normalizedTag.substring(/* beginIndex= */ 3); } } - String normTag = getLocaleLanguageTag(locale); - return toLowerInvariant(localeLanguage + normTag.substring(localeLanguageLength)); + return normalizedTag; } /** @@ -1982,32 +1988,25 @@ public final class Util { } private static String[] getSystemLocales() { + Configuration config = Resources.getSystem().getConfiguration(); return SDK_INT >= 24 - ? getSystemLocalesV24() - : new String[] {getLocaleLanguageTag(Resources.getSystem().getConfiguration().locale)}; + ? getSystemLocalesV24(config) + : SDK_INT >= 21 ? getSystemLocaleV21(config) : new String[] {config.locale.toString()}; } @TargetApi(24) - private static String[] getSystemLocalesV24() { - return Util.split(Resources.getSystem().getConfiguration().getLocales().toLanguageTags(), ","); - } - - private static Locale getLocaleForLanguageTag(String languageTag) { - return Util.SDK_INT >= 21 ? getLocaleForLanguageTagV21(languageTag) : new Locale(languageTag); + private static String[] getSystemLocalesV24(Configuration config) { + return Util.split(config.getLocales().toLanguageTags(), ","); } @TargetApi(21) - private static Locale getLocaleForLanguageTagV21(String languageTag) { - return Locale.forLanguageTag(languageTag); - } - - private static String getLocaleLanguageTag(Locale locale) { - return SDK_INT >= 21 ? getLocaleLanguageTagV21(locale) : locale.toString(); + private static String[] getSystemLocaleV21(Configuration config) { + return new String[] {config.locale.toLanguageTag()}; } @TargetApi(21) - private static String getLocaleLanguageTagV21(Locale locale) { - return locale.toLanguageTag(); + private static String normalizeLanguageCodeSyntaxV21(String languageTag) { + return Locale.forLanguageTag(languageTag).toLanguageTag(); } private static @C.NetworkType int getMobileNetworkType(NetworkInfo networkInfo) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index f85ee37c07..5a13ed0dd8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -268,10 +268,14 @@ public class UtilTest { @Test @Config(sdk = 21) public void testNormalizeLanguageCodeV21() { + assertThat(Util.normalizeLanguageCode(null)).isNull(); + assertThat(Util.normalizeLanguageCode("")).isEmpty(); assertThat(Util.normalizeLanguageCode("es")).isEqualTo("es"); assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es_AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("spa_ar")).isEqualTo("es-ar"); assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("es-ar-dialect"); assertThat(Util.normalizeLanguageCode("ES-419")).isEqualTo("es-419"); assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zh-hans-tw"); @@ -284,9 +288,20 @@ public class UtilTest { @Test @Config(sdk = 16) public void testNormalizeLanguageCode() { + assertThat(Util.normalizeLanguageCode(null)).isNull(); + assertThat(Util.normalizeLanguageCode("")).isEmpty(); assertThat(Util.normalizeLanguageCode("es")).isEqualTo("es"); assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es_AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("spa_ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("es-ar-dialect"); + assertThat(Util.normalizeLanguageCode("ES-419")).isEqualTo("es-419"); + assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zh-hans-tw"); + // Doesn't work on API < 21 because we can't use Locale syntax verification. + // assertThat(Util.normalizeLanguageCode("zh-tw-hans")).isEqualTo("zh-tw"); + assertThat(Util.normalizeLanguageCode("zho-hans-tw")).isEqualTo("zh-hans-tw"); assertThat(Util.normalizeLanguageCode("und")).isEqualTo("und"); assertThat(Util.normalizeLanguageCode("DoesNotExist")).isEqualTo("doesnotexist"); } From 08624113d409a448b4aa227c9532c4eedeeb2266 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 18 Jul 2019 17:40:57 +0100 Subject: [PATCH 1467/1556] Correctly mask playback info changes in ExoPlayerImpl. PlaybackInfo changes are one of the last ones not masked and reported in the same way as all other changes. The main change to support this is to also mask the parameters set in DefaultAudioSink. PiperOrigin-RevId: 258787744 --- .../android/exoplayer2/DefaultMediaClock.java | 11 +-- .../android/exoplayer2/ExoPlayerImpl.java | 27 +++++- .../exoplayer2/ExoPlayerImplInternal.java | 28 ++++-- .../com/google/android/exoplayer2/Player.java | 11 +-- .../android/exoplayer2/audio/AudioSink.java | 7 +- .../exoplayer2/audio/DefaultAudioSink.java | 19 ++-- .../audio/MediaCodecAudioRenderer.java | 4 +- .../audio/SimpleDecoderAudioRenderer.java | 4 +- .../android/exoplayer2/util/MediaClock.java | 9 +- .../exoplayer2/util/StandaloneMediaClock.java | 3 +- .../exoplayer2/DefaultMediaClockTest.java | 48 ++-------- .../android/exoplayer2/ExoPlayerTest.java | 93 ++++++++++++++++++- 12 files changed, 173 insertions(+), 91 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java index bcec6426d6..410dffd558 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java @@ -32,12 +32,12 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; public interface PlaybackParameterListener { /** - * Called when the active playback parameters changed. + * Called when the active playback parameters changed. Will not be called for {@link + * #setPlaybackParameters(PlaybackParameters)}. * * @param newPlaybackParameters The newly active {@link PlaybackParameters}. */ void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters); - } private final StandaloneMediaClock standaloneMediaClock; @@ -141,13 +141,12 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(PlaybackParameters playbackParameters) { if (rendererClock != null) { - playbackParameters = rendererClock.setPlaybackParameters(playbackParameters); + rendererClock.setPlaybackParameters(playbackParameters); + playbackParameters = rendererClock.getPlaybackParameters(); } standaloneMediaClock.setPlaybackParameters(playbackParameters); - listener.onPlaybackParametersChanged(playbackParameters); - return playbackParameters; } @Override 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 f380af968c..3eed66402d 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 @@ -69,6 +69,7 @@ import java.util.concurrent.CopyOnWriteArrayList; private boolean hasPendingPrepare; private boolean hasPendingSeek; private boolean foregroundMode; + private int pendingSetPlaybackParametersAcks; private PlaybackParameters playbackParameters; private SeekParameters seekParameters; @Nullable private ExoPlaybackException playbackError; @@ -336,7 +337,14 @@ import java.util.concurrent.CopyOnWriteArrayList; if (playbackParameters == null) { playbackParameters = PlaybackParameters.DEFAULT; } + if (this.playbackParameters.equals(playbackParameters)) { + return; + } + pendingSetPlaybackParametersAcks++; + this.playbackParameters = playbackParameters; internalPlayer.setPlaybackParameters(playbackParameters); + PlaybackParameters playbackParametersToNotify = playbackParameters; + notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParametersToNotify)); } @Override @@ -560,11 +568,7 @@ import java.util.concurrent.CopyOnWriteArrayList; /* positionDiscontinuityReason= */ msg.arg2); break; case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: - PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj; - if (!this.playbackParameters.equals(playbackParameters)) { - this.playbackParameters = playbackParameters; - notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParameters)); - } + handlePlaybackParameters((PlaybackParameters) msg.obj, /* operationAck= */ msg.arg1 != 0); break; case ExoPlayerImplInternal.MSG_ERROR: ExoPlaybackException playbackError = (ExoPlaybackException) msg.obj; @@ -576,6 +580,19 @@ import java.util.concurrent.CopyOnWriteArrayList; } } + private void handlePlaybackParameters( + PlaybackParameters playbackParameters, boolean operationAck) { + if (operationAck) { + pendingSetPlaybackParametersAcks--; + } + if (pendingSetPlaybackParametersAcks == 0) { + if (!this.playbackParameters.equals(playbackParameters)) { + this.playbackParameters = playbackParameters; + notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParameters)); + } + } + } + private void handlePlaybackInfo( PlaybackInfo playbackInfo, int operationAcks, 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 38cdb57fc8..738a30fad1 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 @@ -297,9 +297,7 @@ import java.util.concurrent.atomic.AtomicBoolean; @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - handler - .obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL, playbackParameters) - .sendToTarget(); + sendPlaybackParametersChangedInternal(playbackParameters, /* acknowledgeCommand= */ false); } // Handler.Callback implementation. @@ -358,7 +356,8 @@ import java.util.concurrent.atomic.AtomicBoolean; reselectTracksInternal(); break; case MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL: - handlePlaybackParameters((PlaybackParameters) msg.obj); + handlePlaybackParameters( + (PlaybackParameters) msg.obj, /* acknowledgeCommand= */ msg.arg1 != 0); break; case MSG_SEND_MESSAGE: sendMessageInternal((PlayerMessage) msg.obj); @@ -783,6 +782,8 @@ import java.util.concurrent.atomic.AtomicBoolean; private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) { mediaClock.setPlaybackParameters(playbackParameters); + sendPlaybackParametersChangedInternal( + mediaClock.getPlaybackParameters(), /* acknowledgeCommand= */ true); } private void setSeekParametersInternal(SeekParameters seekParameters) { @@ -1663,9 +1664,13 @@ import java.util.concurrent.atomic.AtomicBoolean; maybeContinueLoading(); } - private void handlePlaybackParameters(PlaybackParameters playbackParameters) + private void handlePlaybackParameters( + PlaybackParameters playbackParameters, boolean acknowledgeCommand) throws ExoPlaybackException { - eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget(); + eventHandler + .obtainMessage( + MSG_PLAYBACK_PARAMETERS_CHANGED, acknowledgeCommand ? 1 : 0, 0, playbackParameters) + .sendToTarget(); updateTrackSelectionPlaybackSpeed(playbackParameters.speed); for (Renderer renderer : renderers) { if (renderer != null) { @@ -1820,6 +1825,17 @@ import java.util.concurrent.atomic.AtomicBoolean; loadControl.onTracksSelected(renderers, trackGroups, trackSelectorResult.selections); } + private void sendPlaybackParametersChangedInternal( + PlaybackParameters playbackParameters, boolean acknowledgeCommand) { + handler + .obtainMessage( + MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL, + acknowledgeCommand ? 1 : 0, + 0, + playbackParameters) + .sendToTarget(); + } + private static Format[] getFormats(TrackSelection newSelection) { // Build an array of formats contained by the selection. int length = newSelection != null ? newSelection.length() : 0; 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 4e062dcb5e..eed59876f9 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 @@ -761,13 +761,10 @@ public interface Player { /** * Attempts to set the playback parameters. Passing {@code null} sets the parameters to the * default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment. - *

          - * Playback parameters changes may cause the player to buffer. - * {@link EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever - * the currently active playback parameters change. When that listener is called, the parameters - * passed to it may not match {@code playbackParameters}. For example, the chosen speed or pitch - * may be out of range, in which case they are constrained to a set of permitted values. If it is - * not possible to change the playback parameters, the listener will not be invoked. + * + *

          Playback parameters changes may cause the player to buffer. {@link + * EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever the + * currently active playback parameters change. * * @param playbackParameters The playback parameters, or {@code null} to use the defaults. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index 393380453c..f2458a7471 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -259,13 +259,12 @@ public interface AudioSink { boolean hasPendingData(); /** - * Attempts to set the playback parameters and returns the active playback parameters, which may - * differ from those passed in. + * Attempts to set the playback parameters. The audio sink may override these parameters if they + * are not supported. * * @param playbackParameters The new playback parameters to attempt to set. - * @return The active playback parameters. */ - PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters); + void setPlaybackParameters(PlaybackParameters playbackParameters); /** * Gets the active {@link PlaybackParameters}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index e3f753958e..b4e0058982 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -825,17 +825,12 @@ public final class DefaultAudioSink implements AudioSink { } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(PlaybackParameters playbackParameters) { if (configuration != null && !configuration.canApplyPlaybackParameters) { this.playbackParameters = PlaybackParameters.DEFAULT; - return this.playbackParameters; + return; } - PlaybackParameters lastSetPlaybackParameters = - afterDrainPlaybackParameters != null - ? afterDrainPlaybackParameters - : !playbackParametersCheckpoints.isEmpty() - ? playbackParametersCheckpoints.getLast().playbackParameters - : this.playbackParameters; + PlaybackParameters lastSetPlaybackParameters = getPlaybackParameters(); if (!playbackParameters.equals(lastSetPlaybackParameters)) { if (isInitialized()) { // Drain the audio processors so we can determine the frame position at which the new @@ -847,12 +842,16 @@ public final class DefaultAudioSink implements AudioSink { this.playbackParameters = playbackParameters; } } - return this.playbackParameters; } @Override public PlaybackParameters getPlaybackParameters() { - return playbackParameters; + // Mask the already set parameters. + return afterDrainPlaybackParameters != null + ? afterDrainPlaybackParameters + : !playbackParametersCheckpoints.isEmpty() + ? playbackParametersCheckpoints.getLast().playbackParameters + : playbackParameters; } @Override 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 7e889097bc..b965f4ef68 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 @@ -648,8 +648,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - return audioSink.setPlaybackParameters(playbackParameters); + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + audioSink.setPlaybackParameters(playbackParameters); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index ef0207517a..b17fa75181 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -517,8 +517,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - return audioSink.setPlaybackParameters(playbackParameters); + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + audioSink.setPlaybackParameters(playbackParameters); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java index a10298e456..e9f08a35c9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java @@ -28,13 +28,12 @@ public interface MediaClock { long getPositionUs(); /** - * Attempts to set the playback parameters and returns the active playback parameters, which may - * differ from those passed in. + * Attempts to set the playback parameters. The media clock may override these parameters if they + * are not supported. * - * @param playbackParameters The playback parameters. - * @return The active playback parameters. + * @param playbackParameters The playback parameters to attempt to set. */ - PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters); + void setPlaybackParameters(PlaybackParameters playbackParameters); /** * Returns the active playback parameters. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java index b1f53416fb..e5f9aa645f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java @@ -88,13 +88,12 @@ public final class StandaloneMediaClock implements MediaClock { } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(PlaybackParameters playbackParameters) { // Store the current position as the new base, in case the playback speed has changed. if (started) { resetPosition(getPositionUs()); } this.playbackParameters = playbackParameters; - return playbackParameters; } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java index be0f7f55c7..c42edb32ae 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.MockitoAnnotations.initMocks; @@ -116,15 +115,14 @@ public class DefaultMediaClockTest { @Test public void standaloneSetPlaybackParameters_getPlaybackParametersShouldReturnSameValue() { - PlaybackParameters parameters = mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - assertThat(parameters).isEqualTo(TEST_PLAYBACK_PARAMETERS); + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); } @Test - public void standaloneSetPlaybackParameters_shouldTriggerCallback() { + public void standaloneSetPlaybackParameters_shouldNotTriggerCallback() { mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + verifyNoMoreInteractions(listener); } @Test @@ -137,24 +135,9 @@ public class DefaultMediaClockTest { @Test public void standaloneSetOtherPlaybackParameters_getPlaybackParametersShouldReturnSameValue() { - mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - PlaybackParameters parameters = mediaClock.setPlaybackParameters(PlaybackParameters.DEFAULT); - assertThat(parameters).isEqualTo(PlaybackParameters.DEFAULT); - assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); - } - - @Test - public void standaloneSetOtherPlaybackParameters_shouldTriggerCallbackAgain() { mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); mediaClock.setPlaybackParameters(PlaybackParameters.DEFAULT); - verify(listener).onPlaybackParametersChanged(PlaybackParameters.DEFAULT); - } - - @Test - public void standaloneSetSamePlaybackParametersAgain_shouldTriggerCallbackAgain() { - mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - verify(listener, times(2)).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); } @Test @@ -210,19 +193,18 @@ public class DefaultMediaClockTest { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); - PlaybackParameters parameters = mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - assertThat(parameters).isEqualTo(TEST_PLAYBACK_PARAMETERS); + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); } @Test - public void rendererClockSetPlaybackParameters_shouldTriggerCallback() + public void rendererClockSetPlaybackParameters_shouldNotTriggerCallback() throws ExoPlaybackException { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + verifyNoMoreInteractions(listener); } @Test @@ -231,19 +213,8 @@ public class DefaultMediaClockTest { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); - PlaybackParameters parameters = mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - assertThat(parameters).isEqualTo(PlaybackParameters.DEFAULT); - assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); - } - - @Test - public void rendererClockSetPlaybackParametersOverwrite_shouldTriggerCallback() - throws ExoPlaybackException { - FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, - /* playbackParametersAreMutable= */ false); - mediaClock.onRendererEnabled(mediaClockRenderer); mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - verify(listener).onPlaybackParametersChanged(PlaybackParameters.DEFAULT); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); } @Test @@ -418,11 +389,10 @@ public class DefaultMediaClockTest { } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(PlaybackParameters playbackParameters) { if (playbackParametersAreMutable) { this.playbackParameters = playbackParameters; } - return this.playbackParameters; } @Override 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 39046b52ce..d8ba3bcbda 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 @@ -223,9 +223,7 @@ public final class ExoPlayerTest { } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - return PlaybackParameters.DEFAULT; - } + public void setPlaybackParameters(PlaybackParameters playbackParameters) {} @Override public PlaybackParameters getPlaybackParameters() { @@ -2641,6 +2639,95 @@ public final class ExoPlayerTest { assertThat(contentStartPositionMs.get()).isAtLeast(5_000L); } + @Test + public void setPlaybackParametersConsecutivelyNotifiesListenerForEveryChangeOnce() + throws Exception { + ActionSchedule actionSchedule = + new ActionSchedule.Builder("setPlaybackParametersNotifiesListenerForEveryChangeOnce") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .setPlaybackParameters(new PlaybackParameters(1.1f)) + .setPlaybackParameters(new PlaybackParameters(1.2f)) + .setPlaybackParameters(new PlaybackParameters(1.3f)) + .play() + .build(); + List reportedPlaybackParameters = new ArrayList<>(); + EventListener listener = + new EventListener() { + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + reportedPlaybackParameters.add(playbackParameters); + } + }; + new ExoPlayerTestRunner.Builder() + .setActionSchedule(actionSchedule) + .setEventListener(listener) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(reportedPlaybackParameters) + .containsExactly( + new PlaybackParameters(1.1f), + new PlaybackParameters(1.2f), + new PlaybackParameters(1.3f)) + .inOrder(); + } + + @Test + public void + setUnsupportedPlaybackParametersConsecutivelyNotifiesListenerForEveryChangeOnceAndResetsOnceHandled() + throws Exception { + Renderer renderer = + new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) { + @Override + public long getPositionUs() { + return 0; + } + + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) {} + + @Override + public PlaybackParameters getPlaybackParameters() { + return PlaybackParameters.DEFAULT; + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("setUnsupportedPlaybackParametersNotifiesListenersCorrectly") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .setPlaybackParameters(new PlaybackParameters(1.1f)) + .setPlaybackParameters(new PlaybackParameters(1.2f)) + .setPlaybackParameters(new PlaybackParameters(1.3f)) + .play() + .build(); + List reportedPlaybackParameters = new ArrayList<>(); + EventListener listener = + new EventListener() { + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + reportedPlaybackParameters.add(playbackParameters); + } + }; + new ExoPlayerTestRunner.Builder() + .setSupportedFormats(Builder.AUDIO_FORMAT) + .setRenderers(renderer) + .setActionSchedule(actionSchedule) + .setEventListener(listener) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(reportedPlaybackParameters) + .containsExactly( + new PlaybackParameters(1.1f), + new PlaybackParameters(1.2f), + new PlaybackParameters(1.3f), + PlaybackParameters.DEFAULT) + .inOrder(); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { From 421f6e0303d324010a7e2c32c6b5fa9fe36987be Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Jul 2019 18:37:55 +0100 Subject: [PATCH 1468/1556] Add AV1 HDR profile recognition Recognize AV1ProfileMain10HDR when getting codec profile and level. PiperOrigin-RevId: 258799457 --- .../exoplayer2/mediacodec/MediaCodecUtil.java | 14 ++-- .../mediacodec/MediaCodecUtilTest.java | 68 +++++++++++++++++++ 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index df5ca05972..6a967a359b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -25,10 +25,12 @@ import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Pair; import android.util.SparseIntArray; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.ColorInfo; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -255,7 +257,7 @@ public final class MediaCodecUtil { case CODEC_ID_DVH1: return getDolbyVisionProfileAndLevel(format.codecs, parts); case CODEC_ID_AV01: - return getAv1ProfileAndLevel(format.codecs, parts); + return getAv1ProfileAndLevel(format.codecs, parts, format.colorInfo); case CODEC_ID_MP4A: return getAacCodecProfileAndLevel(format.codecs, parts); default: @@ -686,7 +688,8 @@ public final class MediaCodecUtil { return new Pair<>(profile, level); } - private static Pair getAv1ProfileAndLevel(String codec, String[] parts) { + private static Pair getAv1ProfileAndLevel( + String codec, String[] parts, @Nullable ColorInfo colorInfo) { if (parts.length < 4) { Log.w(TAG, "Ignoring malformed AV1 codec string: " + codec); return null; @@ -703,8 +706,6 @@ public final class MediaCodecUtil { return null; } - // TODO: Recognize HDR profiles. Currently, the profile is assumed to be either Main8 or Main10. - // See [Internal: b/124435216]. if (profileInteger != 0) { Log.w(TAG, "Unknown AV1 profile: " + profileInteger); return null; @@ -716,6 +717,11 @@ public final class MediaCodecUtil { int profile; if (bitDepthInteger == 8) { profile = CodecProfileLevel.AV1ProfileMain8; + } else if (colorInfo != null + && (colorInfo.hdrStaticInfo != null + || colorInfo.colorTransfer == C.COLOR_TRANSFER_HLG + || colorInfo.colorTransfer == C.COLOR_TRANSFER_ST2084)) { + profile = CodecProfileLevel.AV1ProfileMain10HDR10; } else { profile = CodecProfileLevel.AV1ProfileMain10; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java index c485ff49f6..e8d65255c3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java @@ -20,8 +20,10 @@ import static com.google.common.truth.Truth.assertThat; import android.media.MediaCodecInfo; import android.util.Pair; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.video.ColorInfo; import org.junit.Test; import org.junit.runner.RunWith; @@ -78,6 +80,68 @@ public final class MediaCodecUtilTest { MediaCodecInfo.CodecProfileLevel.AV1Level7); } + @Test + public void getCodecProfileAndLevel_handlesAv1ProfileMain10HDRWithHdrInfoSet() { + ColorInfo colorInfo = + new ColorInfo( + /* colorSpace= */ C.COLOR_SPACE_BT709, + /* colorRange= */ C.COLOR_RANGE_LIMITED, + /* colorTransfer= */ C.COLOR_TRANSFER_SDR, + /* hdrStaticInfo= */ new byte[] {1, 2, 3, 4, 5, 6, 7}); + Format format = + Format.createVideoSampleFormat( + /* id= */ null, + /* sampleMimeType= */ MimeTypes.VIDEO_UNKNOWN, + /* codecs= */ "av01.0.21M.10", + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + /* width= */ 1024, + /* height= */ 768, + /* frameRate= */ Format.NO_VALUE, + /* initializationData= */ null, + /* rotationDegrees= */ Format.NO_VALUE, + /* pixelWidthHeightRatio= */ 0, + /* projectionData= */ null, + /* stereoMode= */ Format.NO_VALUE, + /* colorInfo= */ colorInfo, + /* drmInitData */ null); + assertCodecProfileAndLevelForFormat( + format, + MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10HDR10, + MediaCodecInfo.CodecProfileLevel.AV1Level71); + } + + @Test + public void getCodecProfileAndLevel_handlesAv1ProfileMain10HDRWithoutHdrInfoSet() { + ColorInfo colorInfo = + new ColorInfo( + /* colorSpace= */ C.COLOR_SPACE_BT709, + /* colorRange= */ C.COLOR_RANGE_LIMITED, + /* colorTransfer= */ C.COLOR_TRANSFER_HLG, + /* hdrStaticInfo= */ null); + Format format = + Format.createVideoSampleFormat( + /* id= */ null, + /* sampleMimeType= */ MimeTypes.VIDEO_UNKNOWN, + /* codecs= */ "av01.0.21M.10", + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + /* width= */ 1024, + /* height= */ 768, + /* frameRate= */ Format.NO_VALUE, + /* initializationData= */ null, + /* rotationDegrees= */ Format.NO_VALUE, + /* pixelWidthHeightRatio= */ 0, + /* projectionData= */ null, + /* stereoMode= */ Format.NO_VALUE, + /* colorInfo= */ colorInfo, + /* drmInitData */ null); + assertCodecProfileAndLevelForFormat( + format, + MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10HDR10, + MediaCodecInfo.CodecProfileLevel.AV1Level71); + } + @Test public void getCodecProfileAndLevel_handlesFullAv1CodecString() { // Example from https://aomediacodec.github.io/av1-isobmff/#codecsparam. @@ -135,6 +199,10 @@ public final class MediaCodecUtilTest { /* frameRate= */ Format.NO_VALUE, /* initializationData= */ null, /* drmInitData= */ null); + assertCodecProfileAndLevelForFormat(format, profile, level); + } + + private static void assertCodecProfileAndLevelForFormat(Format format, int profile, int level) { Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); assertThat(codecProfileAndLevel).isNotNull(); assertThat(codecProfileAndLevel.first).isEqualTo(profile); From 8b554dc30a8d17f1f9d64b72e079e5c2bb82c31e Mon Sep 17 00:00:00 2001 From: Yannick RUI Date: Fri, 19 Jul 2019 08:38:47 +0200 Subject: [PATCH 1469/1556] Improve code readability and fix an issue with text tracks that should not be selected --- .../trackselection/DefaultTrackSelector.java | 62 +++++++++---------- 1 file changed, 30 insertions(+), 32 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 b5d282f8a7..b830fa5b78 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 @@ -2076,7 +2076,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); TextTrackScore trackScore = new TextTrackScore(format, params, trackFormatSupport[trackIndex], selectedAudioLanguage); - if ((selectedTrackScore == null) || trackScore.compareTo(selectedTrackScore) > 0) { + if (trackScore.isWithinConstraints + && ((selectedTrackScore == null) || trackScore.compareTo(selectedTrackScore) > 0)) { selectedGroup = trackGroup; selectedTrackIndex = trackIndex; selectedTrackScore = trackScore; @@ -2514,8 +2515,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final int preferredLanguageScore; private final int selectedAudioLanguageScore; private final boolean trackHasNoLanguage; - private final boolean selectUndeterminedTextLanguage; - private final boolean stringDefinesNoLang; + private final boolean hasLanguageMatch; + private final boolean hasSelectedAudioLanguageMatch; + private final boolean isWithinConstraints; public TextTrackScore( Format format, @@ -2525,13 +2527,17 @@ public class DefaultTrackSelector extends MappingTrackSelector { isWithinRendererCapabilities = isSupported(trackFormatSupport, false); int maskedSelectionFlags = format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags; - isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; + isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; preferredLanguageScore = getFormatLanguageScore(format, parameters.preferredTextLanguage); selectedAudioLanguageScore = getFormatLanguageScore(format, selectedAudioLanguage); trackHasNoLanguage = formatHasNoLanguage(format); - selectUndeterminedTextLanguage = parameters.selectUndeterminedTextLanguage; - stringDefinesNoLang = stringDefinesNoLanguage(selectedAudioLanguage); + hasLanguageMatch = preferredLanguageScore > 0 + || (parameters.selectUndeterminedTextLanguage && trackHasNoLanguage); + hasSelectedAudioLanguageMatch = (selectedAudioLanguageScore > 0) + || (trackHasNoLanguage && stringDefinesNoLanguage(selectedAudioLanguage)); + isWithinConstraints = + (hasLanguageMatch || isDefault || (isForced && hasSelectedAudioLanguageMatch)); } /** @@ -2546,34 +2552,26 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { return this.isWithinRendererCapabilities ? 1 : -1; } - if ((this.preferredLanguageScore > 0 || (this.selectUndeterminedTextLanguage && this.trackHasNoLanguage)) == - (other.preferredLanguageScore > 0 || (other.selectUndeterminedTextLanguage && other.trackHasNoLanguage))) { - if (this.preferredLanguageScore > 0 || (this.selectUndeterminedTextLanguage - && this.trackHasNoLanguage)) { - if (this.isDefault != other.isDefault) { - return this.isDefault ? 1 : -1; - } - if (this.isForced != other.isForced) { - // Prefer non-forced to forced if a preferred text language has been specified. Where - // both are provided the non-forced track will usually contain the forced subtitles as - // a subset. - return !this.isForced ? 1 : -1; - } - return (this.preferredLanguageScore > other.preferredLanguageScore) ? 1 : -1; - } else { - if (this.isDefault != other.isDefault) { - return this.isDefault ? 1 : -1; - } - if ((this.isForced && (this.selectedAudioLanguageScore > 0 || (this.trackHasNoLanguage && this.stringDefinesNoLang))) != - (other.isForced && (other.selectedAudioLanguageScore > 0 || (other.trackHasNoLanguage && other.stringDefinesNoLang)))) { - return (this.isForced && (this.selectedAudioLanguageScore > 0 - || (this.trackHasNoLanguage && this.stringDefinesNoLang))) ? 1 : -1; - } - // Track should not be selected. - return -1; + if (this.hasLanguageMatch != other.hasLanguageMatch) { + return this.hasLanguageMatch ? 1 : -1; + } + if (this.isDefault != other.isDefault) { + return this.isDefault ? 1 : -1; + } + if (this.hasLanguageMatch) { + if (this.isForced != other.isForced) { + // Prefer non-forced to forced if a preferred text language has been specified. Where + // both are provided the non-forced track will usually contain the forced subtitles as + // a subset. + return !this.isForced ? 1 : -1; } + return this.preferredLanguageScore - other.preferredLanguageScore; } else { - return (this.preferredLanguageScore > 0 || (this.selectUndeterminedTextLanguage && this.trackHasNoLanguage)) ? 1 : -1; + if ((this.isForced && this.hasSelectedAudioLanguageMatch) != + (other.isForced && other.hasSelectedAudioLanguageMatch)) { + return (this.isForced && this.hasSelectedAudioLanguageMatch) ? 1 : -1; + } + return 0; } } } From 3a53543a9af343e23d34af6fdfa551d9baf05464 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 22 Jul 2019 19:27:05 +0100 Subject: [PATCH 1470/1556] Move HLS DrmInitData adjustment to the writing side + Emulates what's done for ID3 stripping. + Also avoid a copy if fields will not change because of the copy. PiperOrigin-RevId: 259369101 --- .../com/google/android/exoplayer2/Format.java | 40 +++++-------------- .../source/DecryptableSampleQueueReader.java | 20 ---------- .../source/hls/HlsSampleStreamWrapper.java | 19 +++++++-- 3 files changed, 25 insertions(+), 54 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 df01df1708..b2bd20f0fe 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 @@ -1291,39 +1291,19 @@ public final class Format implements Parcelable { } public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) { - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - subsampleOffsetUs, - width, - height, - frameRate, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - channelCount, - sampleRate, - pcmEncoding, - encoderDelay, - encoderPadding, - language, - accessibilityChannel, - exoMediaCryptoType); + return copyWithAdjustments(drmInitData, metadata); } public Format copyWithMetadata(@Nullable Metadata metadata) { + return copyWithAdjustments(drmInitData, metadata); + } + + @SuppressWarnings("ReferenceEquality") + public Format copyWithAdjustments( + @Nullable DrmInitData drmInitData, @Nullable Metadata metadata) { + if (drmInitData == this.drmInitData && metadata == this.metadata) { + return this; + } return new Format( id, label, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java index b0b10d4e98..365a48cadf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java @@ -27,8 +27,6 @@ import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** @@ -41,7 +39,6 @@ public final class DecryptableSampleQueueReader { private final DrmSessionManager sessionManager; private final FormatHolder formatHolder; private final boolean playClearSamplesWithoutKeys; - private final HashMap overridingDrmInitDatas; private @MonotonicNonNull Format currentFormat; @Nullable private DrmSession currentSession; @@ -58,19 +55,6 @@ public final class DecryptableSampleQueueReader { formatHolder = new FormatHolder(); playClearSamplesWithoutKeys = (sessionManager.getFlags() & DrmSessionManager.FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS) != 0; - overridingDrmInitDatas = new HashMap<>(); - } - - /** - * Given a mapping from {@link DrmInitData#schemeType} to {@link DrmInitData}, overrides any - * {@link DrmInitData} read from the upstream {@link SampleQueue} whose {@link - * DrmInitData#schemeType} is a key in the mapping to use the corresponding {@link DrmInitData} - * value. If {@code overridingDrmInitDatas} does not contain a mapping for the upstream {@link - * DrmInitData#schemeType}, the upstream {@link DrmInitData} is used. - */ - public void setOverridingDrmInitDatas(Map overridingDrmInitDatas) { - this.overridingDrmInitDatas.clear(); - this.overridingDrmInitDatas.putAll(overridingDrmInitDatas); } /** Releases any resources acquired by this reader. */ @@ -192,10 +176,6 @@ public final class DecryptableSampleQueueReader { DrmSession previousSession = currentSession; DrmInitData drmInitData = currentFormat.drmInitData; if (drmInitData != null) { - DrmInitData overridingDrmInitData = overridingDrmInitDatas.get(drmInitData.schemeType); - if (overridingDrmInitData != null) { - drmInitData = overridingDrmInitData; - } currentSession = sessionManager.acquireSession(Assertions.checkNotNull(Looper.myLooper()), drmInitData); } else { 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 079852c4d4..360a7d6f72 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 @@ -828,7 +828,7 @@ import java.util.Set; return createDummyTrackOutput(id, type); } } - SampleQueue trackOutput = new PrivTimestampStrippingSampleQueue(allocator); + SampleQueue trackOutput = new FormatAdjustingSampleQueue(allocator, overridingDrmInitData); trackOutput.setSampleOffsetUs(sampleOffsetUs); trackOutput.sourceId(chunkUid); trackOutput.setUpstreamFormatChangeListener(this); @@ -1170,15 +1170,26 @@ import java.util.Set; return new DummyTrackOutput(); } - private static final class PrivTimestampStrippingSampleQueue extends SampleQueue { + private static final class FormatAdjustingSampleQueue extends SampleQueue { - public PrivTimestampStrippingSampleQueue(Allocator allocator) { + private final Map overridingDrmInitData; + + public FormatAdjustingSampleQueue( + Allocator allocator, Map overridingDrmInitData) { super(allocator); + this.overridingDrmInitData = overridingDrmInitData; } @Override public void format(Format format) { - super.format(format.copyWithMetadata(getAdjustedMetadata(format.metadata))); + DrmInitData drmInitData = format.drmInitData; + if (drmInitData != null) { + DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType); + if (overridingDrmInitData != null) { + drmInitData = overridingDrmInitData; + } + } + super.format(format.copyWithAdjustments(drmInitData, getAdjustedMetadata(format.metadata))); } /** From e6bafec41842a30495018231416a72371417afa7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 23 Jul 2019 08:02:24 +0100 Subject: [PATCH 1471/1556] Deduplicate ID3 header constants PiperOrigin-RevId: 259479785 --- .../android/exoplayer2/extractor/Id3Peeker.java | 2 +- .../exoplayer2/extractor/ts/Ac3Extractor.java | 7 ++++--- .../exoplayer2/extractor/ts/Ac4Extractor.java | 8 ++++---- .../exoplayer2/extractor/ts/AdtsExtractor.java | 8 +++++--- .../android/exoplayer2/extractor/ts/Id3Reader.java | 13 ++++++------- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java index 255799c026..60386dcc3c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java @@ -53,7 +53,7 @@ public final class Id3Peeker { Metadata metadata = null; while (true) { try { - input.peekFully(scratch.data, 0, Id3Decoder.ID3_HEADER_LENGTH); + input.peekFully(scratch.data, /* offset= */ 0, Id3Decoder.ID3_HEADER_LENGTH); } catch (EOFException e) { // If input has less than ID3_HEADER_LENGTH, ignore the rest. break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index 2f46744ea0..b1d15b7189 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.audio.Ac3Util; @@ -44,7 +46,6 @@ public final class Ac3Extractor implements Extractor { private static final int MAX_SNIFF_BYTES = 8 * 1024; private static final int AC3_SYNC_WORD = 0x0B77; private static final int MAX_SYNC_FRAME_SIZE = 2786; - private static final int ID3_TAG = 0x00494433; private final Ac3Reader reader; private final ParsableByteArray sampleData; @@ -62,10 +63,10 @@ public final class Ac3Extractor implements Extractor { @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { // Skip any ID3 headers. - ParsableByteArray scratch = new ParsableByteArray(10); + ParsableByteArray scratch = new ParsableByteArray(ID3_HEADER_LENGTH); int startPosition = 0; while (true) { - input.peekFully(scratch.data, 0, 10); + input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH); scratch.setPosition(0); if (scratch.readUnsignedInt24() != ID3_TAG) { break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java index 2e8dcd952b..205d71e16e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.android.exoplayer2.audio.Ac4Util.AC40_SYNCWORD; import static com.google.android.exoplayer2.audio.Ac4Util.AC41_SYNCWORD; import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.audio.Ac4Util; @@ -52,8 +54,6 @@ public final class Ac4Extractor implements Extractor { /** The size of the frame header, in bytes. */ private static final int FRAME_HEADER_SIZE = 7; - private static final int ID3_TAG = 0x00494433; - private final Ac4Reader reader; private final ParsableByteArray sampleData; @@ -70,10 +70,10 @@ public final class Ac4Extractor implements Extractor { @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { // Skip any ID3 headers. - ParsableByteArray scratch = new ParsableByteArray(10); + ParsableByteArray scratch = new ParsableByteArray(ID3_HEADER_LENGTH); int startPosition = 0; while (true) { - input.peekFully(scratch.data, /* offset= */ 0, /* length= */ 10); + input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH); scratch.setPosition(0); if (scratch.readUnsignedInt24() != ID3_TAG) { break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index 3dd88fbb51..381f19809b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG; import androidx.annotation.IntDef; import androidx.annotation.Nullable; @@ -65,7 +67,6 @@ public final class AdtsExtractor implements Extractor { public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; private static final int MAX_PACKET_SIZE = 2 * 1024; - private static final int ID3_TAG = 0x00494433; /** * The maximum number of bytes to search when sniffing, excluding the header, before giving up. * Frame sizes are represented by 13-bit fields, so expect a valid frame in the first 8192 bytes. @@ -109,7 +110,8 @@ public final class AdtsExtractor implements Extractor { packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); averageFrameSize = C.LENGTH_UNSET; firstFramePosition = C.POSITION_UNSET; - scratch = new ParsableByteArray(10); + // Allocate scratch space for an ID3 header. The same buffer is also used to read 4 byte values. + scratch = new ParsableByteArray(ID3_HEADER_LENGTH); scratchBits = new ParsableBitArray(scratch.data); } @@ -209,7 +211,7 @@ public final class AdtsExtractor implements Extractor { private int peekId3Header(ExtractorInput input) throws IOException, InterruptedException { int firstFramePosition = 0; while (true) { - input.peekFully(scratch.data, 0, 10); + input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH); scratch.setPosition(0); if (scratch.readUnsignedInt24() != ID3_TAG) { break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index f936fb9e43..77ec48d0a7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -33,8 +34,6 @@ public final class Id3Reader implements ElementaryStreamReader { private static final String TAG = "Id3Reader"; - private static final int ID3_HEADER_SIZE = 10; - private final ParsableByteArray id3Header; private TrackOutput output; @@ -48,7 +47,7 @@ public final class Id3Reader implements ElementaryStreamReader { private int sampleBytesRead; public Id3Reader() { - id3Header = new ParsableByteArray(ID3_HEADER_SIZE); + id3Header = new ParsableByteArray(ID3_HEADER_LENGTH); } @Override @@ -81,12 +80,12 @@ public final class Id3Reader implements ElementaryStreamReader { return; } int bytesAvailable = data.bytesLeft(); - if (sampleBytesRead < ID3_HEADER_SIZE) { + if (sampleBytesRead < ID3_HEADER_LENGTH) { // We're still reading the ID3 header. - int headerBytesAvailable = Math.min(bytesAvailable, ID3_HEADER_SIZE - sampleBytesRead); + int headerBytesAvailable = Math.min(bytesAvailable, ID3_HEADER_LENGTH - sampleBytesRead); System.arraycopy(data.data, data.getPosition(), id3Header.data, sampleBytesRead, headerBytesAvailable); - if (sampleBytesRead + headerBytesAvailable == ID3_HEADER_SIZE) { + if (sampleBytesRead + headerBytesAvailable == ID3_HEADER_LENGTH) { // We've finished reading the ID3 header. Extract the sample size. id3Header.setPosition(0); if ('I' != id3Header.readUnsignedByte() || 'D' != id3Header.readUnsignedByte() @@ -96,7 +95,7 @@ public final class Id3Reader implements ElementaryStreamReader { return; } id3Header.skipBytes(3); // version (2) + flags (1) - sampleSize = ID3_HEADER_SIZE + id3Header.readSynchSafeInt(); + sampleSize = ID3_HEADER_LENGTH + id3Header.readSynchSafeInt(); } } // Write data to the output. From 2a8cf2f5efa97bd4bcdc2a25cd93fb17f0d2d922 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 23 Jul 2019 13:53:59 +0100 Subject: [PATCH 1472/1556] Plumb DrmSessionManager into HlsMediaSource PiperOrigin-RevId: 259520431 --- .../exoplayer2/demo/PlayerActivity.java | 4 +- .../exoplayer2/source/hls/HlsMediaPeriod.java | 8 +++ .../exoplayer2/source/hls/HlsMediaSource.java | 23 +++++++ .../source/hls/HlsSampleStream.java | 5 +- .../source/hls/HlsSampleStreamWrapper.java | 63 ++++++++++++++----- .../source/hls/HlsMediaPeriodTest.java | 2 + 6 files changed, 87 insertions(+), 18 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 249223bff5..1f60224b28 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 @@ -484,7 +484,9 @@ public class PlayerActivity extends AppCompatActivity .setDrmSessionManager(drmSessionManager) .createMediaSource(uri); case C.TYPE_HLS: - return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + return new HlsMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); case C.TYPE_OTHER: return new ProgressiveMediaSource.Factory(dataSourceFactory) .setDrmSessionManager(drmSessionManager) 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 39b49da402..e21827557a 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 @@ -22,6 +22,8 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; @@ -63,6 +65,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final HlsPlaylistTracker playlistTracker; private final HlsDataSourceFactory dataSourceFactory; @Nullable private final TransferListener mediaTransferListener; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Allocator allocator; @@ -91,6 +94,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper * and keys. * @param mediaTransferListener The transfer listener to inform of any media data transfers. May * be null if no listener is available. + * @param drmSessionManager The {@link DrmSessionManager} to acquire {@link DrmSession + * DrmSessions} with. * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. * @param eventDispatcher A dispatcher to notify of events. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. @@ -104,6 +109,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper HlsPlaylistTracker playlistTracker, HlsDataSourceFactory dataSourceFactory, @Nullable TransferListener mediaTransferListener, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, Allocator allocator, @@ -114,6 +120,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper this.playlistTracker = playlistTracker; this.dataSourceFactory = dataSourceFactory; this.mediaTransferListener = mediaTransferListener; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.allocator = allocator; @@ -735,6 +742,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper allocator, positionUs, muxedAudioFormat, + drmSessionManager, loadErrorHandlingPolicy, eventDispatcher); } 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 12c6a8ee72..877b6d486e 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 @@ -20,6 +20,8 @@ import android.os.Handler; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.BaseMediaSource; @@ -65,6 +67,7 @@ public final class HlsMediaSource extends BaseMediaSource @Nullable private List streamKeys; private HlsPlaylistTracker.Factory playlistTrackerFactory; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private DrmSessionManager drmSessionManager; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private boolean allowChunklessPreparation; private boolean useSessionKeys; @@ -93,6 +96,7 @@ public final class HlsMediaSource extends BaseMediaSource playlistParserFactory = new DefaultHlsPlaylistParserFactory(); playlistTrackerFactory = DefaultHlsPlaylistTracker.FACTORY; extractorFactory = HlsExtractorFactory.DEFAULT; + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } @@ -127,6 +131,20 @@ public final class HlsMediaSource extends BaseMediaSource return this; } + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManager = drmSessionManager; + return this; + } + /** * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. @@ -271,6 +289,7 @@ public final class HlsMediaSource extends BaseMediaSource hlsDataSourceFactory, extractorFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, playlistTrackerFactory.createTracker( hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory), @@ -297,6 +316,7 @@ public final class HlsMediaSource extends BaseMediaSource private final Uri manifestUri; private final HlsDataSourceFactory dataSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final boolean allowChunklessPreparation; private final boolean useSessionKeys; @@ -310,6 +330,7 @@ public final class HlsMediaSource extends BaseMediaSource HlsDataSourceFactory dataSourceFactory, HlsExtractorFactory extractorFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, HlsPlaylistTracker playlistTracker, boolean allowChunklessPreparation, @@ -319,6 +340,7 @@ public final class HlsMediaSource extends BaseMediaSource this.dataSourceFactory = dataSourceFactory; this.extractorFactory = extractorFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.playlistTracker = playlistTracker; this.allowChunklessPreparation = allowChunklessPreparation; @@ -352,6 +374,7 @@ public final class HlsMediaSource extends BaseMediaSource playlistTracker, dataSourceFactory, mediaTransferListener, + drmSessionManager, loadErrorHandlingPolicy, eventDispatcher, allocator, diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index cf879e91c6..c820038b80 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -62,8 +62,11 @@ import java.io.IOException; if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL) { throw new SampleQueueMappingException( sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType); + } else if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) { + sampleStreamWrapper.maybeThrowError(); + } else if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL) { + sampleStreamWrapper.maybeThrowError(sampleQueueIndex); } - sampleStreamWrapper.maybeThrowError(); } @Override 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 360a7d6f72..c8c1b8f566 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 @@ -23,12 +23,15 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.PrivFrame; +import com.google.android.exoplayer2.source.DecryptableSampleQueueReader; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; @@ -94,6 +97,7 @@ import java.util.Set; private final HlsChunkSource chunkSource; private final Allocator allocator; private final Format muxedAudioFormat; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final Loader loader; private final EventDispatcher eventDispatcher; @@ -107,6 +111,7 @@ import java.util.Set; private final Map overridingDrmInitData; private SampleQueue[] sampleQueues; + private DecryptableSampleQueueReader[] sampleQueueReaders; private int[] sampleQueueTrackIds; private boolean audioSampleQueueMappingDone; private int audioSampleQueueIndex; @@ -154,6 +159,8 @@ import java.util.Set; * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param positionUs The position from which to start loading media. * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist. + * @param drmSessionManager The {@link DrmSessionManager} to acquire {@link DrmSession + * DrmSessions} with. * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. * @param eventDispatcher A dispatcher to notify of events. */ @@ -165,6 +172,7 @@ import java.util.Set; Allocator allocator, long positionUs, Format muxedAudioFormat, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher) { this.trackType = trackType; @@ -173,6 +181,7 @@ import java.util.Set; this.overridingDrmInitData = overridingDrmInitData; this.allocator = allocator; this.muxedAudioFormat = muxedAudioFormat; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; loader = new Loader("Loader:HlsSampleStreamWrapper"); @@ -181,6 +190,7 @@ import java.util.Set; audioSampleQueueIndex = C.INDEX_UNSET; videoSampleQueueIndex = C.INDEX_UNSET; sampleQueues = new SampleQueue[0]; + sampleQueueReaders = new DecryptableSampleQueueReader[0]; sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueuesEnabledStates = new boolean[0]; mediaChunks = new ArrayList<>(); @@ -211,7 +221,7 @@ import java.util.Set; public void prepareWithMasterPlaylistInfo( TrackGroup[] trackGroups, int primaryTrackGroupIndex, int... optionalTrackGroupsIndices) { prepared = true; - this.trackGroups = new TrackGroupArray(trackGroups); + this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups); optionalTrackGroups = new HashSet<>(); for (int optionalTrackGroupIndex : optionalTrackGroupsIndices) { optionalTrackGroups.add(this.trackGroups.get(optionalTrackGroupIndex)); @@ -438,6 +448,9 @@ import java.util.Set; for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.discardToEnd(); } + for (DecryptableSampleQueueReader reader : sampleQueueReaders) { + reader.release(); + } } loader.release(this); handler.removeCallbacksAndMessages(null); @@ -448,6 +461,9 @@ import java.util.Set; @Override public void onLoaderReleased() { resetSampleQueues(); + for (DecryptableSampleQueueReader reader : sampleQueueReaders) { + reader.release(); + } } public void setIsTimestampMaster(boolean isTimestampMaster) { @@ -461,7 +477,12 @@ import java.util.Set; // SampleStream implementation. public boolean isReady(int sampleQueueIndex) { - return loadingFinished || (!isPendingReset() && sampleQueues[sampleQueueIndex].hasNextSample()); + return !isPendingReset() && sampleQueueReaders[sampleQueueIndex].isReady(loadingFinished); + } + + public void maybeThrowError(int sampleQueueIndex) throws IOException { + maybeThrowError(); + sampleQueueReaders[sampleQueueIndex].maybeThrowError(); } public void maybeThrowError() throws IOException { @@ -494,13 +515,8 @@ import java.util.Set; } int result = - sampleQueues[sampleQueueIndex].read( - formatHolder, - buffer, - requireFormat, - /* allowOnlyClearBuffers= */ false, - loadingFinished, - lastSeekPositionUs); + sampleQueueReaders[sampleQueueIndex].read( + formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs); if (result == C.RESULT_FORMAT_READ) { Format format = formatHolder.format; if (sampleQueueIndex == primarySampleQueueIndex) { @@ -516,12 +532,6 @@ import java.util.Set; : upstreamTrackFormat; format = format.copyWithManifestFormatInfo(trackFormat); } - if (format.drmInitData != null) { - DrmInitData drmInitData = overridingDrmInitData.get(format.drmInitData.schemeType); - if (drmInitData != null) { - format = format.copyWithDrmInitData(drmInitData); - } - } formatHolder.format = format; } return result; @@ -836,6 +846,9 @@ import java.util.Set; sampleQueueTrackIds[trackCount] = id; sampleQueues = Arrays.copyOf(sampleQueues, trackCount + 1); sampleQueues[trackCount] = trackOutput; + sampleQueueReaders = Arrays.copyOf(sampleQueueReaders, trackCount + 1); + sampleQueueReaders[trackCount] = + new DecryptableSampleQueueReader(sampleQueues[trackCount], drmSessionManager); sampleQueueIsAudioVideoFlags = Arrays.copyOf(sampleQueueIsAudioVideoFlags, trackCount + 1); sampleQueueIsAudioVideoFlags[trackCount] = type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO; @@ -1048,11 +1061,29 @@ import java.util.Set; trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat, false)); } } - this.trackGroups = new TrackGroupArray(trackGroups); + this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups); Assertions.checkState(optionalTrackGroups == null); optionalTrackGroups = Collections.emptySet(); } + private TrackGroupArray createTrackGroupArrayWithDrmInfo(TrackGroup[] trackGroups) { + for (int i = 0; i < trackGroups.length; i++) { + TrackGroup trackGroup = trackGroups[i]; + Format[] exposedFormats = new Format[trackGroup.length]; + for (int j = 0; j < trackGroup.length; j++) { + Format format = trackGroup.getFormat(j); + if (format.drmInitData != null) { + format = + format.copyWithExoMediaCryptoType( + drmSessionManager.getExoMediaCryptoType(format.drmInitData)); + } + exposedFormats[j] = format; + } + trackGroups[i] = new TrackGroup(exposedFormats); + } + return new TrackGroupArray(trackGroups); + } + private HlsMediaChunk getLastMediaChunk() { return mediaChunks.get(mediaChunks.size() - 1); } diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java index f389944670..93b8be3346 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.when; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -81,6 +82,7 @@ public final class HlsMediaPeriodTest { mockPlaylistTracker, mockDataSourceFactory, mock(TransferListener.class), + mock(DrmSessionManager.class), mock(LoadErrorHandlingPolicy.class), new EventDispatcher() .withParameters( From 3c3777d4debacbd714acde2e264470aee6eba445 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 23 Jul 2019 14:20:00 +0100 Subject: [PATCH 1473/1556] Fix release of DecryptableSampleQueueReaders in ProgressiveMediaPeriod PiperOrigin-RevId: 259523450 --- .../exoplayer2/source/ProgressiveMediaPeriod.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index 83145d04b0..d25fff5104 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -200,9 +200,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.discardToEnd(); } - } - for (DecryptableSampleQueueReader reader : sampleQueueReaders) { - reader.release(); + for (DecryptableSampleQueueReader reader : sampleQueueReaders) { + reader.release(); + } } loader.release(/* callback= */ this); handler.removeCallbacksAndMessages(null); @@ -216,6 +216,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.reset(); } + for (DecryptableSampleQueueReader reader : sampleQueueReaders) { + reader.release(); + } extractorHolder.release(); } From e5b3c32c983bb5b4c328a6793b50f167104afef8 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 23 Jul 2019 15:07:43 +0100 Subject: [PATCH 1474/1556] Remove DrmSessionManager from Renderer creation in the main demo app PiperOrigin-RevId: 259529691 --- .../com/google/android/exoplayer2/demo/PlayerActivity.java | 3 +-- 1 file changed, 1 insertion(+), 2 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 1f60224b28..40b1a94991 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 @@ -416,8 +416,7 @@ public class PlayerActivity extends AppCompatActivity lastSeenTrackGroupArray = null; player = - ExoPlayerFactory.newSimpleInstance( - /* context= */ this, renderersFactory, trackSelector, drmSessionManager); + ExoPlayerFactory.newSimpleInstance(/* context= */ this, renderersFactory, trackSelector); player.addListener(new PlayerEventListener()); player.setPlayWhenReady(startAutoPlay); player.addAnalyticsListener(new EventLogger(trackSelector)); From 39574b5a614b134ae65b6d95175ae659daa0eddc Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 23 Jul 2019 15:33:29 +0100 Subject: [PATCH 1475/1556] Make one of the ExoPlayerTest tests more sensible. Some variables were defined although they are the default and other things were set-up in a non-sensible way, e.g. asserting that audio is selected although no audio renderer is available, or using unset duration for everything. PiperOrigin-RevId: 259532782 --- .../android/exoplayer2/ExoPlayerTest.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) 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 d8ba3bcbda..d5b0b2c667 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 @@ -51,7 +51,6 @@ import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.TransferListener; @@ -2253,17 +2252,15 @@ public final class ExoPlayerTest { public void testUpdateTrackSelectorThenSeekToUnpreparedPeriod_returnsEmptyTrackGroups() throws Exception { // Use unset duration to prevent pre-loading of the second window. - Timeline fakeTimeline = + Timeline timelineUnsetDuration = new FakeTimeline( new TimelineWindowDefinition( /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ C.TIME_UNSET)); - MediaSource[] fakeMediaSources = { - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, Builder.AUDIO_FORMAT) - }; - MediaSource mediaSource = new ConcatenatingMediaSource(fakeMediaSources); - FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); - DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + Timeline timelineSetDuration = new FakeTimeline(/* windowCount= */ 1); + MediaSource mediaSource = + new ConcatenatingMediaSource( + new FakeMediaSource(timelineUnsetDuration, Builder.VIDEO_FORMAT), + new FakeMediaSource(timelineSetDuration, Builder.AUDIO_FORMAT)); ActionSchedule actionSchedule = new ActionSchedule.Builder("testUpdateTrackSelectorThenSeekToUnpreparedPeriod") .pause() @@ -2275,8 +2272,7 @@ public final class ExoPlayerTest { List trackSelectionsList = new ArrayList<>(); new Builder() .setMediaSource(mediaSource) - .setTrackSelector(trackSelector) - .setRenderers(renderer) + .setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT) .setActionSchedule(actionSchedule) .setEventListener( new EventListener() { From 2c318d7b8472917a11930659fa1afa1bdd057cf7 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 23 Jul 2019 19:59:55 +0100 Subject: [PATCH 1476/1556] Cast: Remove obsolete flavor dimension PiperOrigin-RevId: 259582498 --- demos/cast/build.gradle | 11 ----------- demos/cast/src/main/AndroidManifest.xml | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index 03a54947cf..85e60f2796 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -47,17 +47,6 @@ android { // The demo app isn't indexed and doesn't have translations. disable 'GoogleAppIndexingWarning','MissingTranslation' } - - flavorDimensions "receiver" - - productFlavors { - defaultCast { - dimension "receiver" - manifestPlaceholders = - [castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"] - } - } - } dependencies { diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index 856b0b1235..dbfdd833f6 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -25,7 +25,7 @@ android:largeHeap="true" android:allowBackup="false"> + android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"/> Date: Tue, 23 Jul 2019 20:21:10 +0100 Subject: [PATCH 1477/1556] Cast extension: Remove unused parts of MediaItem PiperOrigin-RevId: 259586520 --- .../exoplayer2/castdemo/MainActivity.java | 16 +- .../exoplayer2/ext/cast/MediaItem.java | 140 +----------------- .../exoplayer2/ext/cast/MediaItemTest.java | 66 ++------- 3 files changed, 21 insertions(+), 201 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index c17c0a62ab..2adfb28632 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -51,8 +51,6 @@ import java.util.Collections; public class MainActivity extends AppCompatActivity implements OnClickListener, PlayerManager.Listener { - private final MediaItem.Builder mediaItemBuilder; - private PlayerView localPlayerView; private PlayerControlView castControlView; private PlayerManager playerManager; @@ -60,10 +58,6 @@ public class MainActivity extends AppCompatActivity private MediaQueueListAdapter mediaQueueListAdapter; private CastContext castContext; - public MainActivity() { - mediaItemBuilder = new MediaItem.Builder(); - } - // Activity lifecycle methods. @Override @@ -179,11 +173,11 @@ public class MainActivity extends AppCompatActivity sampleList.setOnItemClickListener( (parent, view, position, id) -> { DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position); - mediaItemBuilder - .clear() - .setMedia(sample.uri) - .setTitle(sample.name) - .setMimeType(sample.mimeType); + MediaItem.Builder mediaItemBuilder = + new MediaItem.Builder() + .setMedia(sample.uri) + .setTitle(sample.name) + .setMimeType(sample.mimeType); DemoUtil.DrmConfiguration drmConfiguration = sample.drmConfiguration; if (drmConfiguration != null) { mediaItemBuilder.setDrmSchemes( diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java index adb8e59070..79f36f46d7 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java @@ -17,8 +17,6 @@ package com.google.android.exoplayer2.ext.cast; import android.net.Uri; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Collections; @@ -26,8 +24,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; -import org.checkerframework.checker.initialization.qual.UnknownInitialization; -import org.checkerframework.checker.nullness.qual.EnsuresNonNull; /** Representation of an item that can be played by a media player. */ public final class MediaItem { @@ -35,25 +31,16 @@ public final class MediaItem { /** A builder for {@link MediaItem} instances. */ public static final class Builder { - @Nullable private UUID uuid; private String title; - private String description; private MediaItem.UriBundle media; - @Nullable private Object attachment; private List drmSchemes; - private long startPositionUs; - private long endPositionUs; private String mimeType; - /** Creates an builder with default field values. */ public Builder() { - clearInternal(); - } - - /** See {@link MediaItem#uuid}. */ - public Builder setUuid(UUID uuid) { - this.uuid = uuid; - return this; + title = ""; + media = UriBundle.EMPTY; + drmSchemes = Collections.emptyList(); + mimeType = ""; } /** See {@link MediaItem#title}. */ @@ -62,12 +49,6 @@ public final class MediaItem { return this; } - /** See {@link MediaItem#description}. */ - public Builder setDescription(String description) { - this.description = description; - return this; - } - /** Equivalent to {@link #setMedia(UriBundle) setMedia(new UriBundle(Uri.parse(uri)))}. */ public Builder setMedia(String uri) { return setMedia(new UriBundle(Uri.parse(uri))); @@ -79,84 +60,26 @@ public final class MediaItem { return this; } - /** See {@link MediaItem#attachment}. */ - public Builder setAttachment(Object attachment) { - this.attachment = attachment; - return this; - } - /** See {@link MediaItem#drmSchemes}. */ public Builder setDrmSchemes(List drmSchemes) { this.drmSchemes = Collections.unmodifiableList(new ArrayList<>(drmSchemes)); return this; } - /** See {@link MediaItem#startPositionUs}. */ - public Builder setStartPositionUs(long startPositionUs) { - this.startPositionUs = startPositionUs; - return this; - } - - /** See {@link MediaItem#endPositionUs}. */ - public Builder setEndPositionUs(long endPositionUs) { - Assertions.checkArgument(endPositionUs != C.TIME_END_OF_SOURCE); - this.endPositionUs = endPositionUs; - return this; - } - /** See {@link MediaItem#mimeType}. */ public Builder setMimeType(String mimeType) { this.mimeType = mimeType; return this; } - /** - * Equivalent to {@link #build()}, except it also calls {@link #clear()} after creating the - * {@link MediaItem}. - */ - public MediaItem buildAndClear() { - MediaItem item = build(); - clearInternal(); - return item; - } - - /** Returns the builder to default values. */ - public Builder clear() { - clearInternal(); - return this; - } - - /** - * Returns a new {@link MediaItem} instance with the current builder values. This method also - * clears any values passed to {@link #setUuid(UUID)}. - */ + /** Returns a new {@link MediaItem} instance with the current builder values. */ public MediaItem build() { - UUID uuid = this.uuid; - this.uuid = null; return new MediaItem( - uuid != null ? uuid : UUID.randomUUID(), title, - description, media, - attachment, drmSchemes, - startPositionUs, - endPositionUs, mimeType); } - - @EnsuresNonNull({"title", "description", "media", "drmSchemes", "mimeType"}) - private void clearInternal(@UnknownInitialization Builder this) { - uuid = null; - title = ""; - description = ""; - media = UriBundle.EMPTY; - attachment = null; - drmSchemes = Collections.emptyList(); - startPositionUs = C.TIME_UNSET; - endPositionUs = C.TIME_UNSET; - mimeType = ""; - } } /** Bundles a resource's URI with headers to attach to any request to that URI. */ @@ -259,49 +182,20 @@ public final class MediaItem { } } - /** - * A UUID that identifies this item, potentially across different devices. The default value is - * obtained by calling {@link UUID#randomUUID()}. - */ - public final UUID uuid; - /** The title of the item. The default value is an empty string. */ public final String title; - /** A description for the item. The default value is an empty string. */ - public final String description; - /** * A {@link UriBundle} to fetch the media content. The default value is {@link UriBundle#EMPTY}. */ public final UriBundle media; - /** - * An optional opaque object to attach to the media item. Handling of this attachment is - * implementation specific. The default value is null. - */ - @Nullable public final Object attachment; - /** * Immutable list of {@link DrmScheme} instances sorted in decreasing order of preference. The * default value is an empty list. */ public final List drmSchemes; - /** - * The position in microseconds at which playback of this media item should start. {@link - * C#TIME_UNSET} if playback should start at the default position. The default value is {@link - * C#TIME_UNSET}. - */ - public final long startPositionUs; - - /** - * The position in microseconds at which playback of this media item should end. {@link - * C#TIME_UNSET} if playback should end at the end of the media. The default value is {@link - * C#TIME_UNSET}. - */ - public final long endPositionUs; - /** * The mime type of this media item. The default value is an empty string. * @@ -320,49 +214,29 @@ public final class MediaItem { return false; } MediaItem mediaItem = (MediaItem) other; - return startPositionUs == mediaItem.startPositionUs - && endPositionUs == mediaItem.endPositionUs - && uuid.equals(mediaItem.uuid) - && title.equals(mediaItem.title) - && description.equals(mediaItem.description) + return title.equals(mediaItem.title) && media.equals(mediaItem.media) - && Util.areEqual(attachment, mediaItem.attachment) && drmSchemes.equals(mediaItem.drmSchemes) && mimeType.equals(mediaItem.mimeType); } @Override public int hashCode() { - int result = uuid.hashCode(); - result = 31 * result + title.hashCode(); - result = 31 * result + description.hashCode(); + int result = title.hashCode(); result = 31 * result + media.hashCode(); - result = 31 * result + (attachment != null ? attachment.hashCode() : 0); result = 31 * result + drmSchemes.hashCode(); - result = 31 * result + (int) (startPositionUs ^ (startPositionUs >>> 32)); - result = 31 * result + (int) (endPositionUs ^ (endPositionUs >>> 32)); result = 31 * result + mimeType.hashCode(); return result; } private MediaItem( - UUID uuid, String title, - String description, UriBundle media, - @Nullable Object attachment, List drmSchemes, - long startPositionUs, - long endPositionUs, String mimeType) { - this.uuid = uuid; this.title = title; - this.description = description; this.media = media; - this.attachment = attachment; this.drmSchemes = drmSchemes; - this.startPositionUs = startPositionUs; - this.endPositionUs = endPositionUs; this.mimeType = mimeType; } } diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java index 9cdc073b06..d21e57efd1 100644 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java @@ -24,7 +24,6 @@ import com.google.android.exoplayer2.util.MimeTypes; import java.util.Arrays; import java.util.HashMap; import java.util.List; -import java.util.UUID; import org.junit.Test; import org.junit.runner.RunWith; @@ -32,32 +31,16 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class MediaItemTest { - @Test - public void buildMediaItem_resetsUuid() { - MediaItem.Builder builder = new MediaItem.Builder(); - UUID uuid = new UUID(1, 1); - MediaItem item1 = builder.setUuid(uuid).build(); - MediaItem item2 = builder.build(); - MediaItem item3 = builder.build(); - assertThat(item1.uuid).isEqualTo(uuid); - assertThat(item2.uuid).isNotEqualTo(uuid); - assertThat(item3.uuid).isNotEqualTo(item2.uuid); - assertThat(item3.uuid).isNotEqualTo(uuid); - } - @Test public void buildMediaItem_doesNotChangeState() { MediaItem.Builder builder = new MediaItem.Builder(); MediaItem item1 = builder - .setUuid(new UUID(0, 1)) .setMedia("http://example.com") .setTitle("title") .setMimeType(MimeTypes.AUDIO_MP4) - .setStartPositionUs(3) - .setEndPositionUs(4) .build(); - MediaItem item2 = builder.setUuid(new UUID(0, 1)).build(); + MediaItem item2 = builder.build(); assertThat(item1).isEqualTo(item2); } @@ -66,63 +49,32 @@ public class MediaItemTest { assertDefaultValues(new MediaItem.Builder().build()); } - @Test - public void buildAndClear_assertDefaultValues() { - MediaItem.Builder builder = new MediaItem.Builder(); - builder - .setMedia("http://example.com") - .setTitle("title") - .setMimeType(MimeTypes.AUDIO_MP4) - .setStartPositionUs(3) - .setEndPositionUs(4) - .buildAndClear(); - assertDefaultValues(builder.build()); - } - @Test public void equals_withEqualDrmSchemes_returnsTrue() { - MediaItem.Builder builder = new MediaItem.Builder(); + MediaItem.Builder builder1 = new MediaItem.Builder(); MediaItem mediaItem1 = - builder - .setUuid(new UUID(0, 1)) - .setMedia("www.google.com") - .setDrmSchemes(createDummyDrmSchemes(1)) - .buildAndClear(); + builder1.setMedia("www.google.com").setDrmSchemes(createDummyDrmSchemes(1)).build(); + MediaItem.Builder builder2 = new MediaItem.Builder(); MediaItem mediaItem2 = - builder - .setUuid(new UUID(0, 1)) - .setMedia("www.google.com") - .setDrmSchemes(createDummyDrmSchemes(1)) - .buildAndClear(); + builder2.setMedia("www.google.com").setDrmSchemes(createDummyDrmSchemes(1)).build(); assertThat(mediaItem1).isEqualTo(mediaItem2); } @Test public void equals_withDifferentDrmRequestHeaders_returnsFalse() { - MediaItem.Builder builder = new MediaItem.Builder(); + MediaItem.Builder builder1 = new MediaItem.Builder(); MediaItem mediaItem1 = - builder - .setUuid(new UUID(0, 1)) - .setMedia("www.google.com") - .setDrmSchemes(createDummyDrmSchemes(1)) - .buildAndClear(); + builder1.setMedia("www.google.com").setDrmSchemes(createDummyDrmSchemes(1)).build(); + MediaItem.Builder builder2 = new MediaItem.Builder(); MediaItem mediaItem2 = - builder - .setUuid(new UUID(0, 1)) - .setMedia("www.google.com") - .setDrmSchemes(createDummyDrmSchemes(2)) - .buildAndClear(); + builder2.setMedia("www.google.com").setDrmSchemes(createDummyDrmSchemes(2)).build(); assertThat(mediaItem1).isNotEqualTo(mediaItem2); } private static void assertDefaultValues(MediaItem item) { assertThat(item.title).isEmpty(); - assertThat(item.description).isEmpty(); assertThat(item.media.uri).isEqualTo(Uri.EMPTY); - assertThat(item.attachment).isNull(); assertThat(item.drmSchemes).isEmpty(); - assertThat(item.startPositionUs).isEqualTo(C.TIME_UNSET); - assertThat(item.endPositionUs).isEqualTo(C.TIME_UNSET); assertThat(item.mimeType).isEmpty(); } From 5e88621ab00d1192c73f4dc792b8d16f136b93ad Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 23 Jul 2019 22:12:49 +0100 Subject: [PATCH 1478/1556] Make LibopusAudioRenderer non-final PiperOrigin-RevId: 259608495 --- .../android/exoplayer2/ext/opus/LibopusAudioRenderer.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index fe74f5c59c..b8b9598989 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -26,10 +26,8 @@ import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; -/** - * Decodes and renders audio using the native Opus decoder. - */ -public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { +/** Decodes and renders audio using the native Opus decoder. */ +public class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { /** The number of input and output buffers. */ private static final int NUM_BUFFERS = 16; From 7d2bfdfc626aab2e561d25818b5910ee738ebc1f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 24 Jul 2019 08:23:28 +0100 Subject: [PATCH 1479/1556] Add AudioFocusGain IntDef in AudioFocusManager PiperOrigin-RevId: 259687632 --- .../google/android/exoplayer2/audio/AudioFocusManager.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java index 2d65b64f36..44bcdfd495 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java @@ -105,9 +105,9 @@ public final class AudioFocusManager { private final PlayerControl playerControl; @Nullable private AudioAttributes audioAttributes; - private @AudioFocusState int audioFocusState; - private int focusGain; - private float volumeMultiplier = 1.0f; + @AudioFocusState private int audioFocusState; + @C.AudioFocusGain private int focusGain; + private float volumeMultiplier = VOLUME_MULTIPLIER_DEFAULT; private @MonotonicNonNull AudioFocusRequest audioFocusRequest; private boolean rebuildAudioFocusRequest; @@ -310,6 +310,7 @@ public final class AudioFocusManager { * @param audioAttributes The audio attributes associated with this focus request. * @return The type of audio focus gain that should be requested. */ + @C.AudioFocusGain private static int convertAudioAttributesToFocusGain(@Nullable AudioAttributes audioAttributes) { if (audioAttributes == null) { From e84d88e90f62d1755317cd692496bac58c5b07d3 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 24 Jul 2019 11:05:27 +0100 Subject: [PATCH 1480/1556] Simplify and improve text selection logic. This changes the logic in the following ways: - If no preferred language is matched, prefer better scores for the selected audio language. - If a preferred language is matched, always prefer the better match irrespective of default or forced flags. - If a preferred language score and the isForced flag is the same, prefer tracks with a better selected audio language match. PiperOrigin-RevId: 259707430 --- RELEASENOTES.md | 2 ++ .../trackselection/DefaultTrackSelector.java | 35 ++++++++----------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5a9070ecf2..176c786682 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -34,6 +34,8 @@ ([#6153](https://github.com/google/ExoPlayer/issues/6153)). * Add ability to specify a description when creating notification channels via ExoPlayer library classes. +* Improve text selection logic to always prefer the better language matches + over other selection parameters. ### 2.10.3 ### 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 c1c0c5cbc7..56eebfbee4 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 @@ -2536,9 +2536,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final boolean isWithinRendererCapabilities; private final boolean isDefault; - private final boolean isForced; + private final boolean hasPreferredIsForcedFlag; private final int preferredLanguageScore; - private final boolean isForcedAndSelectedAudioLanguage; + private final int selectedAudioLanguageScore; public TextTrackScore( Format format, @@ -2550,17 +2550,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { int maskedSelectionFlags = format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags; isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; - isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; + boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; preferredLanguageScore = getFormatLanguageScore( format, parameters.preferredTextLanguage, parameters.selectUndeterminedTextLanguage); + // Prefer non-forced to forced if a preferred text language has been matched. Where both are + // provided the non-forced track will usually contain the forced subtitles as a subset. + // Otherwise, prefer a forced track. + hasPreferredIsForcedFlag = + (preferredLanguageScore > 0 && !isForced) || (preferredLanguageScore == 0 && isForced); boolean selectedAudioLanguageUndetermined = normalizeUndeterminedLanguageToNull(selectedAudioLanguage) == null; - int selectedAudioLanguageScore = + selectedAudioLanguageScore = getFormatLanguageScore(format, selectedAudioLanguage, selectedAudioLanguageUndetermined); - isForcedAndSelectedAudioLanguage = isForced && selectedAudioLanguageScore > 0; isWithinConstraints = - preferredLanguageScore > 0 || isDefault || isForcedAndSelectedAudioLanguage; + preferredLanguageScore > 0 || isDefault || (isForced && selectedAudioLanguageScore > 0); } /** @@ -2575,25 +2579,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { return this.isWithinRendererCapabilities ? 1 : -1; } - if ((this.preferredLanguageScore > 0) != (other.preferredLanguageScore > 0)) { - return this.preferredLanguageScore > 0 ? 1 : -1; + if (this.preferredLanguageScore != other.preferredLanguageScore) { + return compareInts(this.preferredLanguageScore, other.preferredLanguageScore); } if (this.isDefault != other.isDefault) { return this.isDefault ? 1 : -1; } - if (this.preferredLanguageScore > 0) { - if (this.isForced != other.isForced) { - // Prefer non-forced to forced if a preferred text language has been specified. Where - // both are provided the non-forced track will usually contain the forced subtitles as - // a subset. - return !this.isForced ? 1 : -1; - } - return compareInts(this.preferredLanguageScore, other.preferredLanguageScore); + if (this.hasPreferredIsForcedFlag != other.hasPreferredIsForcedFlag) { + return this.hasPreferredIsForcedFlag ? 1 : -1; } - if (this.isForcedAndSelectedAudioLanguage != other.isForcedAndSelectedAudioLanguage) { - return this.isForcedAndSelectedAudioLanguage ? 1 : -1; - } - return 0; + return compareInts(this.selectedAudioLanguageScore, other.selectedAudioLanguageScore); } } } From a0ca79abcc97f010cc829b05d9e98e5524aed450 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 24 Jul 2019 12:17:05 +0100 Subject: [PATCH 1481/1556] Fix doc for preferred audio and text language. Both tags allow any BCP47 compliant code, not just the ISO 639-2/T ones. PiperOrigin-RevId: 259714587 --- .../TrackSelectionParameters.java | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java index 66a4707496..81af551b68 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java @@ -31,9 +31,7 @@ public class TrackSelectionParameters implements Parcelable { */ public static class Builder { - // Audio @Nullable /* package */ String preferredAudioLanguage; - // Text @Nullable /* package */ String preferredTextLanguage; /* package */ boolean selectUndeterminedTextLanguage; @C.SelectionFlags /* package */ int disabledTextTrackSelectionFlags; @@ -48,9 +46,7 @@ public class TrackSelectionParameters implements Parcelable { * the builder are obtained. */ /* package */ Builder(TrackSelectionParameters initialValues) { - // Audio preferredAudioLanguage = initialValues.preferredAudioLanguage; - // Text preferredTextLanguage = initialValues.preferredTextLanguage; selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage; disabledTextTrackSelectionFlags = initialValues.disabledTextTrackSelectionFlags; @@ -67,8 +63,6 @@ public class TrackSelectionParameters implements Parcelable { return this; } - // Text - /** * See {@link TrackSelectionParameters#preferredTextLanguage}. * @@ -117,15 +111,14 @@ public class TrackSelectionParameters implements Parcelable { public static final TrackSelectionParameters DEFAULT = new TrackSelectionParameters(); /** - * 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}. + * The preferred language for audio and forced text tracks as an IETF BCP 47 conformant tag. + * {@code null} selects the default track, or the first track if there's no default. The default + * value is {@code null}. */ @Nullable public final 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. The default value is {@code null}. + * The preferred language for text tracks as an IETF BCP 47 conformant tag. {@code null} selects + * the default track if there is one, or no track otherwise. The default value is {@code null}. */ @Nullable public final String preferredTextLanguage; /** @@ -163,9 +156,7 @@ public class TrackSelectionParameters implements Parcelable { } /* package */ TrackSelectionParameters(Parcel in) { - // Audio this.preferredAudioLanguage = in.readString(); - // Text this.preferredTextLanguage = in.readString(); this.selectUndeterminedTextLanguage = Util.readBoolean(in); this.disabledTextTrackSelectionFlags = in.readInt(); @@ -187,7 +178,6 @@ public class TrackSelectionParameters implements Parcelable { } TrackSelectionParameters other = (TrackSelectionParameters) obj; return TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) - // Text && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage) && selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage && disabledTextTrackSelectionFlags == other.disabledTextTrackSelectionFlags; @@ -196,9 +186,7 @@ public class TrackSelectionParameters implements Parcelable { @Override public int hashCode() { int result = 1; - // Audio result = 31 * result + (preferredAudioLanguage == null ? 0 : preferredAudioLanguage.hashCode()); - // Text result = 31 * result + (preferredTextLanguage == null ? 0 : preferredTextLanguage.hashCode()); result = 31 * result + (selectUndeterminedTextLanguage ? 1 : 0); result = 31 * result + disabledTextTrackSelectionFlags; @@ -214,9 +202,7 @@ public class TrackSelectionParameters implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - // Audio dest.writeString(preferredAudioLanguage); - // Text dest.writeString(preferredTextLanguage); Util.writeBoolean(dest, selectUndeterminedTextLanguage); dest.writeInt(disabledTextTrackSelectionFlags); From 59331c3c887ce8d42b76aa2e1c46106028f862dc Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 24 Jul 2019 15:34:07 +0100 Subject: [PATCH 1482/1556] Report mediaPeriodCreated/Released in MaskingMediaSource. Creating a period in MaskingMediaSource may result in delayed event reporting depending on when the actual period gets created. To avoid event reporting inaccuracies, report the mediaPeriodCreated and mediaPeriodReleased events directly. Issue:#5407 PiperOrigin-RevId: 259737170 --- .../source/CompositeMediaSource.java | 30 +++++++++++++++---- .../exoplayer2/source/MaskingMediaSource.java | 21 +++++++++++-- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java index 3672c304cc..4ebe97313b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java @@ -96,11 +96,11 @@ public abstract class CompositeMediaSource extends BaseMediaSource { /** * Prepares a child source. * - *

          {@link #onChildSourceInfoRefreshed(Object, MediaSource, Timeline)} will be called when the - * child source updates its timeline with the same {@code id} passed to this method. + *

          {@link #onChildSourceInfoRefreshed(T, MediaSource, Timeline)} will be called when the child + * source updates its timeline with the same {@code id} passed to this method. * - *

          Any child sources that aren't explicitly released with {@link #releaseChildSource(Object)} - * will be released in {@link #releaseSourceInternal()}. + *

          Any child sources that aren't explicitly released with {@link #releaseChildSource(T)} will + * be released in {@link #releaseSourceInternal()}. * * @param id A unique id to identify the child source preparation. Null is allowed as an id. * @param mediaSource The child {@link MediaSource}. @@ -188,6 +188,18 @@ public abstract class CompositeMediaSource extends BaseMediaSource { return mediaTimeMs; } + /** + * Returns whether {@link MediaSourceEventListener#onMediaPeriodCreated(int, MediaPeriodId)} and + * {@link MediaSourceEventListener#onMediaPeriodReleased(int, MediaPeriodId)} events of the given + * media period should be reported. The default implementation is to always report these events. + * + * @param mediaPeriodId A {@link MediaPeriodId} in the composite media source. + * @return Whether create and release events for this media period should be reported. + */ + protected boolean shouldDispatchCreateOrReleaseEvent(MediaPeriodId mediaPeriodId) { + return true; + } + private static final class MediaSourceAndListener { public final MediaSource mediaSource; @@ -215,14 +227,20 @@ public abstract class CompositeMediaSource extends BaseMediaSource { @Override public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.mediaPeriodCreated(); + if (shouldDispatchCreateOrReleaseEvent( + Assertions.checkNotNull(eventDispatcher.mediaPeriodId))) { + eventDispatcher.mediaPeriodCreated(); + } } } @Override public void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.mediaPeriodReleased(); + if (shouldDispatchCreateOrReleaseEvent( + Assertions.checkNotNull(eventDispatcher.mediaPeriodId))) { + eventDispatcher.mediaPeriodReleased(); + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java index 1fca824910..d9dd83de4f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java @@ -19,8 +19,10 @@ import androidx.annotation.Nullable; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -37,6 +39,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { private MaskingTimeline timeline; @Nullable private MaskingMediaPeriod unpreparedMaskingMediaPeriod; + @Nullable private EventDispatcher unpreparedMaskingMediaPeriodEventDispatcher; private boolean hasStartedPreparing; private boolean isPrepared; @@ -96,6 +99,9 @@ public final class MaskingMediaSource extends CompositeMediaSource { // unset and we don't load beyond periods with unset duration. We need to figure out how to // handle the prepare positions of multiple deferred media periods, should that ever change. unpreparedMaskingMediaPeriod = mediaPeriod; + unpreparedMaskingMediaPeriodEventDispatcher = + createEventDispatcher(/* windowIndex= */ 0, id, /* mediaTimeOffsetMs= */ 0); + unpreparedMaskingMediaPeriodEventDispatcher.mediaPeriodCreated(); if (!hasStartedPreparing) { hasStartedPreparing = true; prepareChildSource(/* id= */ null, mediaSource); @@ -107,7 +113,11 @@ public final class MaskingMediaSource extends CompositeMediaSource { @Override public void releasePeriod(MediaPeriod mediaPeriod) { ((MaskingMediaPeriod) mediaPeriod).releasePeriod(); - unpreparedMaskingMediaPeriod = null; + if (mediaPeriod == unpreparedMaskingMediaPeriod) { + Assertions.checkNotNull(unpreparedMaskingMediaPeriodEventDispatcher).mediaPeriodReleased(); + unpreparedMaskingMediaPeriodEventDispatcher = null; + unpreparedMaskingMediaPeriod = null; + } } @Override @@ -154,7 +164,6 @@ public final class MaskingMediaSource extends CompositeMediaSource { timeline = MaskingTimeline.createWithRealTimeline(newTimeline, periodUid); if (unpreparedMaskingMediaPeriod != null) { MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod; - unpreparedMaskingMediaPeriod = null; maskingPeriod.overridePreparePositionUs(periodPositionUs); MediaPeriodId idInSource = maskingPeriod.id.copyWithPeriodUid(getInternalPeriodUid(maskingPeriod.id.periodUid)); @@ -172,6 +181,14 @@ public final class MaskingMediaSource extends CompositeMediaSource { return mediaPeriodId.copyWithPeriodUid(getExternalPeriodUid(mediaPeriodId.periodUid)); } + @Override + protected boolean shouldDispatchCreateOrReleaseEvent(MediaPeriodId mediaPeriodId) { + // Suppress create and release events for the period created while the source was still + // unprepared, as we send these events from this class. + return unpreparedMaskingMediaPeriod == null + || !mediaPeriodId.equals(unpreparedMaskingMediaPeriod.id); + } + private Object getInternalPeriodUid(Object externalPeriodUid) { return externalPeriodUid.equals(MaskingTimeline.DUMMY_EXTERNAL_ID) ? timeline.replacedInternalId From 074307dd4c4a591dd851afe3a7ce315505585ffc Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 24 Jul 2019 15:34:29 +0100 Subject: [PATCH 1483/1556] Improve knowledge of last playing period in AnalyticsCollector. We keep track of the last publicly known playing period to report it as part of events happening after the period has been released. In cases where a period briefly becomes the playing one and is released immediately afterwards, we currently don't save it as the "last known playing one". Improve that by saving an explicit reference. Issue:#5407 PiperOrigin-RevId: 259737218 --- .../android/exoplayer2/MediaPeriodQueue.java | 2 +- .../analytics/AnalyticsCollector.java | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) 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 0dacd4df30..2597cd9b3f 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 @@ -277,8 +277,8 @@ import com.google.android.exoplayer2.util.Assertions; if (front != null) { oldFrontPeriodUid = keepFrontPeriodUid ? front.uid : null; oldFrontPeriodWindowSequenceNumber = front.info.id.windowSequenceNumber; - front.release(); removeAfter(front); + front.release(); } else if (!keepFrontPeriodUid) { oldFrontPeriodUid = null; } 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 de0f177342..091696f8bf 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 @@ -686,6 +686,7 @@ public class AnalyticsCollector private final HashMap mediaPeriodIdToInfo; private final Period period; + @Nullable private MediaPeriodInfo lastPlayingMediaPeriod; @Nullable private MediaPeriodInfo lastReportedPlayingMediaPeriod; @Nullable private MediaPeriodInfo readingMediaPeriod; private Timeline timeline; @@ -780,7 +781,7 @@ public class AnalyticsCollector /** Updates the queue with a reported position discontinuity . */ public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - updateLastReportedPlayingMediaPeriod(); + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; } /** Updates the queue with a reported timeline change. */ @@ -795,7 +796,7 @@ public class AnalyticsCollector readingMediaPeriod = updateMediaPeriodInfoToNewTimeline(readingMediaPeriod, timeline); } this.timeline = timeline; - updateLastReportedPlayingMediaPeriod(); + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; } /** Updates the queue with a reported start of seek. */ @@ -806,7 +807,7 @@ public class AnalyticsCollector /** Updates the queue with a reported processed seek. */ public void onSeekProcessed() { isSeeking = false; - updateLastReportedPlayingMediaPeriod(); + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; } /** Updates the queue with a newly created media period. */ @@ -816,8 +817,9 @@ public class AnalyticsCollector new MediaPeriodInfo(mediaPeriodId, isInTimeline ? timeline : Timeline.EMPTY, windowIndex); mediaPeriodInfoQueue.add(mediaPeriodInfo); mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo); + lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0); if (mediaPeriodInfoQueue.size() == 1 && !timeline.isEmpty()) { - updateLastReportedPlayingMediaPeriod(); + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; } } @@ -835,6 +837,9 @@ public class AnalyticsCollector if (readingMediaPeriod != null && mediaPeriodId.equals(readingMediaPeriod.mediaPeriodId)) { readingMediaPeriod = mediaPeriodInfoQueue.isEmpty() ? null : mediaPeriodInfoQueue.get(0); } + if (!mediaPeriodInfoQueue.isEmpty()) { + lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0); + } return true; } @@ -843,12 +848,6 @@ public class AnalyticsCollector readingMediaPeriod = mediaPeriodIdToInfo.get(mediaPeriodId); } - private void updateLastReportedPlayingMediaPeriod() { - if (!mediaPeriodInfoQueue.isEmpty()) { - lastReportedPlayingMediaPeriod = mediaPeriodInfoQueue.get(0); - } - } - private MediaPeriodInfo updateMediaPeriodInfoToNewTimeline( MediaPeriodInfo info, Timeline newTimeline) { int newPeriodIndex = newTimeline.getIndexOfPeriod(info.mediaPeriodId.periodUid); From 1b9e2497ff0fc451f31a155424b17347047187af Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Thu, 25 Jul 2019 11:42:11 +0100 Subject: [PATCH 1484/1556] Add comment about AV1 levels PiperOrigin-RevId: 259918196 --- .../google/android/exoplayer2/mediacodec/MediaCodecUtil.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 6a967a359b..e8fead61ae 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -1060,6 +1060,8 @@ public final class MediaCodecUtil { DOLBY_VISION_STRING_TO_LEVEL.put("08", CodecProfileLevel.DolbyVisionLevelUhd48); DOLBY_VISION_STRING_TO_LEVEL.put("09", CodecProfileLevel.DolbyVisionLevelUhd60); + // See https://aomediacodec.github.io/av1-spec/av1-spec.pdf Annex A: Profiles and levels for + // more information on mapping AV1 codec strings to levels. AV1_LEVEL_NUMBER_TO_CONST = new SparseIntArray(); AV1_LEVEL_NUMBER_TO_CONST.put(0, CodecProfileLevel.AV1Level2); AV1_LEVEL_NUMBER_TO_CONST.put(1, CodecProfileLevel.AV1Level21); From 596be3b71ba1e37fe02dd82b13d3042d97711ff9 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 25 Jul 2019 22:32:04 +0100 Subject: [PATCH 1485/1556] Cast: Simplify MediaItem/Sample to a single MediaItem class PiperOrigin-RevId: 260021990 --- .../android/exoplayer2/castdemo/DemoUtil.java | 88 ++----- .../exoplayer2/castdemo/MainActivity.java | 31 +-- .../exoplayer2/castdemo/PlayerManager.java | 38 +-- .../exoplayer2/ext/cast/MediaItem.java | 225 ++++++------------ .../exoplayer2/ext/cast/MediaItemTest.java | 56 ++--- 5 files changed, 148 insertions(+), 290 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index 2d5a5f0ccf..91ea0c92e2 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -15,97 +15,49 @@ */ package com.google.android.exoplayer2.castdemo; -import androidx.annotation.Nullable; +import android.net.Uri; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ext.cast.MediaItem; +import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.UUID; /** Utility methods and constants for the Cast demo application. */ /* package */ final class DemoUtil { - /** Represents a media sample. */ - public static final class Sample { - - /** The URI of the media content. */ - public final String uri; - /** The name of the sample. */ - public final String name; - /** The mime type of the sample media content. */ - public final String mimeType; - /** Data to configure DRM license acquisition. May be null if content is not DRM-protected. */ - @Nullable public final DrmConfiguration drmConfiguration; - - public Sample(String uri, String name, String mimeType) { - this(uri, name, mimeType, /* drmConfiguration= */ null); - } - - public Sample( - String uri, String name, String mimeType, @Nullable DrmConfiguration drmConfiguration) { - this.uri = uri; - this.name = name; - this.mimeType = mimeType; - this.drmConfiguration = drmConfiguration; - } - - @Override - public String toString() { - return name; - } - } - - /** Holds information required to play DRM-protected content. */ - public static final class DrmConfiguration { - - /** The {@link UUID} of the DRM scheme that protects the content. */ - public final UUID drmSchemeUuid; - /** - * The URI from which players should obtain DRM licenses. May be null if the license server URI - * is provided as part of the media. - */ - @Nullable public final String licenseServerUri; - /** HTTP request headers to include the in DRM license requests. */ - public final Map httpRequestHeaders; - - public DrmConfiguration( - UUID drmSchemeUuid, - @Nullable String licenseServerUri, - Map httpRequestHeaders) { - this.drmSchemeUuid = drmSchemeUuid; - this.licenseServerUri = licenseServerUri; - this.httpRequestHeaders = httpRequestHeaders; - } - } - public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD; public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8; public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS; public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4; /** The list of samples available in the cast demo app. */ - public static final List SAMPLES; + public static final List SAMPLES; static { // App samples. - ArrayList samples = new ArrayList<>(); + ArrayList samples = new ArrayList<>(); // Clear content. samples.add( - new Sample( - "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", - "Clear DASH: Tears", - MIME_TYPE_DASH)); + new MediaItem.Builder() + .setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd") + .setTitle("Clear DASH: Tears") + .setMimeType(MIME_TYPE_DASH) + .build()); samples.add( - new Sample( - "https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8", - "Clear HLS: Angel one", - MIME_TYPE_HLS)); + new MediaItem.Builder() + .setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8") + .setTitle("Clear HLS: Angel one") + .setMimeType(MIME_TYPE_HLS) + .build()); samples.add( - new Sample( - "https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4)); + new MediaItem.Builder() + .setUri("https://html5demos.com/assets/dizzy.mp4") + .setTitle("Clear MP4: Dizzy") + .setMimeType(MIME_TYPE_VIDEO_MP4) + .build()); SAMPLES = Collections.unmodifiableList(samples); } diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 2adfb28632..1a7f28cd77 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.castdemo; import android.content.Context; -import android.net.Uri; import android.os.Bundle; import androidx.core.graphics.ColorUtils; import androidx.appcompat.app.AlertDialog; @@ -42,7 +41,6 @@ import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.dynamite.DynamiteModule; -import java.util.Collections; /** * An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's @@ -172,25 +170,7 @@ public class MainActivity extends AppCompatActivity sampleList.setAdapter(new SampleListAdapter(this)); sampleList.setOnItemClickListener( (parent, view, position, id) -> { - DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position); - MediaItem.Builder mediaItemBuilder = - new MediaItem.Builder() - .setMedia(sample.uri) - .setTitle(sample.name) - .setMimeType(sample.mimeType); - DemoUtil.DrmConfiguration drmConfiguration = sample.drmConfiguration; - if (drmConfiguration != null) { - mediaItemBuilder.setDrmSchemes( - Collections.singletonList( - new MediaItem.DrmScheme( - drmConfiguration.drmSchemeUuid, - new MediaItem.UriBundle( - drmConfiguration.licenseServerUri != null - ? Uri.parse(drmConfiguration.licenseServerUri) - : Uri.EMPTY, - drmConfiguration.httpRequestHeaders)))); - } - playerManager.addItem(mediaItemBuilder.build()); + playerManager.addItem(DemoUtil.SAMPLES.get(position)); mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); }); return dialogList; @@ -213,8 +193,10 @@ public class MainActivity extends AppCompatActivity TextView view = holder.textView; view.setText(holder.item.title); // TODO: Solve coloring using the theme's ColorStateList. - view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(), - position == playerManager.getCurrentItemIndex() ? 255 : 100)); + view.setTextColor( + ColorUtils.setAlphaComponent( + view.getCurrentTextColor(), + position == playerManager.getCurrentItemIndex() ? 255 : 100)); } @Override @@ -294,11 +276,10 @@ public class MainActivity extends AppCompatActivity } } - private static final class SampleListAdapter extends ArrayAdapter { + private static final class SampleListAdapter extends ArrayAdapter { public SampleListAdapter(Context context) { super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); } } - } 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 d2a1ca0860..b877ac7593 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 @@ -44,7 +44,6 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaQueueItem; @@ -368,8 +367,12 @@ import org.json.JSONObject; } private static MediaSource buildMediaSource(MediaItem item) { - Uri uri = item.media.uri; - switch (item.mimeType) { + Uri uri = item.uri; + String mimeType = item.mimeType; + if (mimeType == null) { + throw new IllegalArgumentException("mimeType is required"); + } + switch (mimeType) { case DemoUtil.MIME_TYPE_SS: return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); case DemoUtil.MIME_TYPE_DASH: @@ -379,7 +382,7 @@ import org.json.JSONObject; case DemoUtil.MIME_TYPE_VIDEO_MP4: return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); default: - throw new IllegalStateException("Unsupported type: " + item.mimeType); + throw new IllegalArgumentException("mimeType is unsupported: " + mimeType); } } @@ -387,18 +390,18 @@ import org.json.JSONObject; MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title); MediaInfo.Builder mediaInfoBuilder = - new MediaInfo.Builder(item.media.uri.toString()) + new MediaInfo.Builder(item.uri.toString()) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setContentType(item.mimeType) .setMetadata(movieMetadata); - if (!item.drmSchemes.isEmpty()) { - MediaItem.DrmScheme scheme = item.drmSchemes.get(0); + MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; + if (drmConfiguration != null) { try { // This configuration is only intended for testing and should *not* be used in production // environments. See comment in the Cast Demo app's options provider. - JSONObject drmConfiguration = getDrmConfigurationJson(scheme); - if (drmConfiguration != null) { - mediaInfoBuilder.setCustomData(drmConfiguration); + JSONObject drmConfigurationJson = getDrmConfigurationJson(drmConfiguration); + if (drmConfigurationJson != null) { + mediaInfoBuilder.setCustomData(drmConfigurationJson); } } catch (JSONException e) { throw new RuntimeException(e); @@ -408,24 +411,23 @@ import org.json.JSONObject; } @Nullable - private static JSONObject getDrmConfigurationJson(MediaItem.DrmScheme scheme) + private static JSONObject getDrmConfigurationJson(MediaItem.DrmConfiguration drmConfiguration) throws JSONException { String drmScheme; - if (C.WIDEVINE_UUID.equals(scheme.uuid)) { + if (C.WIDEVINE_UUID.equals(drmConfiguration.uuid)) { drmScheme = "widevine"; - } else if (C.PLAYREADY_UUID.equals(scheme.uuid)) { + } else if (C.PLAYREADY_UUID.equals(drmConfiguration.uuid)) { drmScheme = "playready"; } else { return null; } - MediaItem.UriBundle licenseServer = Assertions.checkNotNull(scheme.licenseServer); JSONObject exoplayerConfig = new JSONObject().put("withCredentials", false).put("protectionSystem", drmScheme); - if (!licenseServer.uri.equals(Uri.EMPTY)) { - exoplayerConfig.put("licenseUrl", licenseServer.uri.toString()); + if (drmConfiguration.licenseUri != null) { + exoplayerConfig.put("licenseUrl", drmConfiguration.licenseUri); } - if (!licenseServer.requestHeaders.isEmpty()) { - exoplayerConfig.put("headers", new JSONObject(licenseServer.requestHeaders)); + if (!drmConfiguration.requestHeaders.isEmpty()) { + exoplayerConfig.put("headers", new JSONObject(drmConfiguration.requestHeaders)); } return new JSONObject().put("exoPlayerConfig", exoplayerConfig); } diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java index 79f36f46d7..7ac0da7078 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java @@ -17,30 +17,32 @@ package com.google.android.exoplayer2.ext.cast; import android.net.Uri; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; -import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.UUID; -/** Representation of an item that can be played by a media player. */ +/** Representation of a media item. */ public final class MediaItem { /** A builder for {@link MediaItem} instances. */ public static final class Builder { - private String title; - private MediaItem.UriBundle media; - private List drmSchemes; - private String mimeType; + @Nullable private Uri uri; + @Nullable private String title; + @Nullable private String mimeType; + @Nullable private DrmConfiguration drmConfiguration; - public Builder() { - title = ""; - media = UriBundle.EMPTY; - drmSchemes = Collections.emptyList(); - mimeType = ""; + /** See {@link MediaItem#uri}. */ + public Builder setUri(String uri) { + return setUri(Uri.parse(uri)); + } + + /** See {@link MediaItem#uri}. */ + public Builder setUri(Uri uri) { + this.uri = uri; + return this; } /** See {@link MediaItem#title}. */ @@ -49,194 +51,125 @@ public final class MediaItem { return this; } - /** Equivalent to {@link #setMedia(UriBundle) setMedia(new UriBundle(Uri.parse(uri)))}. */ - public Builder setMedia(String uri) { - return setMedia(new UriBundle(Uri.parse(uri))); - } - - /** See {@link MediaItem#media}. */ - public Builder setMedia(UriBundle media) { - this.media = media; - return this; - } - - /** See {@link MediaItem#drmSchemes}. */ - public Builder setDrmSchemes(List drmSchemes) { - this.drmSchemes = Collections.unmodifiableList(new ArrayList<>(drmSchemes)); - return this; - } - /** See {@link MediaItem#mimeType}. */ public Builder setMimeType(String mimeType) { this.mimeType = mimeType; return this; } + /** See {@link MediaItem#drmConfiguration}. */ + public Builder setDrmConfiguration(DrmConfiguration drmConfiguration) { + this.drmConfiguration = drmConfiguration; + return this; + } + /** Returns a new {@link MediaItem} instance with the current builder values. */ public MediaItem build() { - return new MediaItem( - title, - media, - drmSchemes, - mimeType); + Assertions.checkNotNull(uri); + return new MediaItem(uri, title, mimeType, drmConfiguration); } } - /** Bundles a resource's URI with headers to attach to any request to that URI. */ - public static final class UriBundle { - - /** An empty {@link UriBundle}. */ - public static final UriBundle EMPTY = new UriBundle(Uri.EMPTY); - - /** A URI. */ - public final Uri uri; - - /** The headers to attach to any request for the given URI. */ - public final Map requestHeaders; - - /** - * Creates an instance with no request headers. - * - * @param uri See {@link #uri}. - */ - public UriBundle(Uri uri) { - this(uri, Collections.emptyMap()); - } - - /** - * Creates an instance with the given URI and request headers. - * - * @param uri See {@link #uri}. - * @param requestHeaders See {@link #requestHeaders}. - */ - public UriBundle(Uri uri, Map requestHeaders) { - this.uri = uri; - this.requestHeaders = Collections.unmodifiableMap(new HashMap<>(requestHeaders)); - } - - @Override - public boolean equals(@Nullable Object other) { - if (this == other) { - return true; - } - if (other == null || getClass() != other.getClass()) { - return false; - } - - UriBundle uriBundle = (UriBundle) other; - return uri.equals(uriBundle.uri) && requestHeaders.equals(uriBundle.requestHeaders); - } - - @Override - public int hashCode() { - int result = uri.hashCode(); - result = 31 * result + requestHeaders.hashCode(); - return result; - } - } - - /** - * Represents a DRM protection scheme, and optionally provides information about how to acquire - * the license for the media. - */ - public static final class DrmScheme { + /** DRM configuration for a media item. */ + public static final class DrmConfiguration { /** The UUID of the protection scheme. */ public final UUID uuid; /** - * Optional {@link UriBundle} for the license server. If no license server is provided, the - * server must be provided by the media. + * Optional license server {@link Uri}. If {@code null} then the license server must be + * specified by the media. */ - @Nullable public final UriBundle licenseServer; + @Nullable public final Uri licenseUri; + + /** Headers that should be attached to any license requests. */ + public final Map requestHeaders; /** * Creates an instance. * * @param uuid See {@link #uuid}. - * @param licenseServer See {@link #licenseServer}. + * @param licenseUri See {@link #licenseUri}. + * @param requestHeaders See {@link #requestHeaders}. */ - public DrmScheme(UUID uuid, @Nullable UriBundle licenseServer) { + public DrmConfiguration( + UUID uuid, @Nullable Uri licenseUri, @Nullable Map requestHeaders) { this.uuid = uuid; - this.licenseServer = licenseServer; + this.licenseUri = licenseUri; + this.requestHeaders = + requestHeaders == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(requestHeaders); } @Override - public boolean equals(@Nullable Object other) { - if (this == other) { + public boolean equals(@Nullable Object obj) { + if (this == obj) { return true; } - if (other == null || getClass() != other.getClass()) { + if (obj == null || getClass() != obj.getClass()) { return false; } - DrmScheme drmScheme = (DrmScheme) other; - return uuid.equals(drmScheme.uuid) && Util.areEqual(licenseServer, drmScheme.licenseServer); + DrmConfiguration other = (DrmConfiguration) obj; + return uuid.equals(other.uuid) + && Util.areEqual(licenseUri, other.licenseUri) + && requestHeaders.equals(other.requestHeaders); } @Override public int hashCode() { int result = uuid.hashCode(); - result = 31 * result + (licenseServer != null ? licenseServer.hashCode() : 0); + result = 31 * result + (licenseUri != null ? licenseUri.hashCode() : 0); + result = 31 * result + requestHeaders.hashCode(); return result; } } - /** The title of the item. The default value is an empty string. */ - public final String title; + /** The media {@link Uri}. */ + public final Uri uri; - /** - * A {@link UriBundle} to fetch the media content. The default value is {@link UriBundle#EMPTY}. - */ - public final UriBundle media; + /** The title of the item, or {@code null} if unspecified. */ + @Nullable public final String title; - /** - * Immutable list of {@link DrmScheme} instances sorted in decreasing order of preference. The - * default value is an empty list. - */ - public final List drmSchemes; + /** The mime type for the media, or {@code null} if unspecified. */ + @Nullable public final String mimeType; - /** - * The mime type of this media item. The default value is an empty string. - * - *

          The usage of this mime type is optional and player implementation specific. - */ - public final String mimeType; + /** Optional {@link DrmConfiguration} for the media. */ + @Nullable public final DrmConfiguration drmConfiguration; - // TODO: Add support for sideloaded tracks, artwork, icon, and subtitle. + private MediaItem( + Uri uri, + @Nullable String title, + @Nullable String mimeType, + @Nullable DrmConfiguration drmConfiguration) { + this.uri = uri; + this.title = title; + this.mimeType = mimeType; + this.drmConfiguration = drmConfiguration; + } @Override - public boolean equals(@Nullable Object other) { - if (this == other) { + public boolean equals(@Nullable Object obj) { + if (this == obj) { return true; } - if (other == null || getClass() != other.getClass()) { + if (obj == null || getClass() != obj.getClass()) { return false; } - MediaItem mediaItem = (MediaItem) other; - return title.equals(mediaItem.title) - && media.equals(mediaItem.media) - && drmSchemes.equals(mediaItem.drmSchemes) - && mimeType.equals(mediaItem.mimeType); + MediaItem other = (MediaItem) obj; + return uri.equals(other.uri) + && Util.areEqual(title, other.title) + && Util.areEqual(mimeType, other.mimeType) + && Util.areEqual(drmConfiguration, other.drmConfiguration); } @Override public int hashCode() { - int result = title.hashCode(); - result = 31 * result + media.hashCode(); - result = 31 * result + drmSchemes.hashCode(); - result = 31 * result + mimeType.hashCode(); + int result = uri.hashCode(); + result = 31 * result + (title == null ? 0 : title.hashCode()); + result = 31 * result + (drmConfiguration == null ? 0 : drmConfiguration.hashCode()); + result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode()); return result; } - - private MediaItem( - String title, - UriBundle media, - List drmSchemes, - String mimeType) { - this.title = title; - this.media = media; - this.drmSchemes = drmSchemes; - this.mimeType = mimeType; - } } diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java index d21e57efd1..7b410a8fbc 100644 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java @@ -21,9 +21,7 @@ import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.MimeTypes; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; @@ -36,7 +34,7 @@ public class MediaItemTest { MediaItem.Builder builder = new MediaItem.Builder(); MediaItem item1 = builder - .setMedia("http://example.com") + .setUri(Uri.parse("http://example.com")) .setTitle("title") .setMimeType(MimeTypes.AUDIO_MP4) .build(); @@ -44,19 +42,20 @@ public class MediaItemTest { assertThat(item1).isEqualTo(item2); } - @Test - public void buildMediaItem_assertDefaultValues() { - assertDefaultValues(new MediaItem.Builder().build()); - } - @Test public void equals_withEqualDrmSchemes_returnsTrue() { MediaItem.Builder builder1 = new MediaItem.Builder(); MediaItem mediaItem1 = - builder1.setMedia("www.google.com").setDrmSchemes(createDummyDrmSchemes(1)).build(); + builder1 + .setUri(Uri.parse("www.google.com")) + .setDrmConfiguration(buildDrmConfiguration(1)) + .build(); MediaItem.Builder builder2 = new MediaItem.Builder(); MediaItem mediaItem2 = - builder2.setMedia("www.google.com").setDrmSchemes(createDummyDrmSchemes(1)).build(); + builder2 + .setUri(Uri.parse("www.google.com")) + .setDrmConfiguration(buildDrmConfiguration(1)) + .build(); assertThat(mediaItem1).isEqualTo(mediaItem2); } @@ -64,33 +63,24 @@ public class MediaItemTest { public void equals_withDifferentDrmRequestHeaders_returnsFalse() { MediaItem.Builder builder1 = new MediaItem.Builder(); MediaItem mediaItem1 = - builder1.setMedia("www.google.com").setDrmSchemes(createDummyDrmSchemes(1)).build(); + builder1 + .setUri(Uri.parse("www.google.com")) + .setDrmConfiguration(buildDrmConfiguration(1)) + .build(); MediaItem.Builder builder2 = new MediaItem.Builder(); MediaItem mediaItem2 = - builder2.setMedia("www.google.com").setDrmSchemes(createDummyDrmSchemes(2)).build(); + builder2 + .setUri(Uri.parse("www.google.com")) + .setDrmConfiguration(buildDrmConfiguration(2)) + .build(); assertThat(mediaItem1).isNotEqualTo(mediaItem2); } - private static void assertDefaultValues(MediaItem item) { - assertThat(item.title).isEmpty(); - assertThat(item.media.uri).isEqualTo(Uri.EMPTY); - assertThat(item.drmSchemes).isEmpty(); - assertThat(item.mimeType).isEmpty(); - } - - private static List createDummyDrmSchemes(int seed) { - HashMap requestHeaders1 = new HashMap<>(); - requestHeaders1.put("key1", "value1"); - requestHeaders1.put("key2", "value1"); - MediaItem.UriBundle uriBundle1 = - new MediaItem.UriBundle(Uri.parse("www.uri1.com"), requestHeaders1); - MediaItem.DrmScheme drmScheme1 = new MediaItem.DrmScheme(C.WIDEVINE_UUID, uriBundle1); - HashMap requestHeaders2 = new HashMap<>(); - requestHeaders2.put("key3", "value3"); - requestHeaders2.put("key4", "valueWithSeed" + seed); - MediaItem.UriBundle uriBundle2 = - new MediaItem.UriBundle(Uri.parse("www.uri2.com"), requestHeaders2); - MediaItem.DrmScheme drmScheme2 = new MediaItem.DrmScheme(C.PLAYREADY_UUID, uriBundle2); - return Arrays.asList(drmScheme1, drmScheme2); + private static MediaItem.DrmConfiguration buildDrmConfiguration(int seed) { + HashMap requestHeaders = new HashMap<>(); + requestHeaders.put("key1", "value1"); + requestHeaders.put("key2", "value2" + seed); + return new MediaItem.DrmConfiguration( + C.WIDEVINE_UUID, Uri.parse("www.uri1.com"), requestHeaders); } } From 9c41bcfe2445bc188b83fe354a7262bde4294b94 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 Jul 2019 16:08:56 +0100 Subject: [PATCH 1486/1556] Add A10-70L to output surface workaround Issue: #6222 PiperOrigin-RevId: 260146226 --- .../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 6e3114d1b1..24524d057d 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 @@ -1487,6 +1487,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { case "1713": case "1714": case "A10-70F": + case "A10-70L": case "A1601": case "A2016a40": case "A7000-a": From 6f7b765a1cf9ba78bf5c1c793d03fc2c754c2ede Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 26 Jul 2019 17:20:45 +0100 Subject: [PATCH 1487/1556] Fix handling of channel count changes with speed adjustment When using speed adjustment it was possible for playback to get stuck at a period transition when the channel count changed: SonicAudioProcessor would be drained at the point of the period transition in preparation for creating a new AudioTrack with the new channel count, but during draining the incorrect (new) channel count was used to calculate the output buffer size for pending data from Sonic. This meant that, for example, if the channel count changed from stereo to mono we could have an output buffer size that stored an non-integer number of audio frames, and in turn this would cause writing to the AudioTrack to get stuck as the AudioTrack would prevent writing a partial audio frame. Use Sonic's current channel count when draining output to fix the issue. PiperOrigin-RevId: 260156541 --- .../java/com/google/android/exoplayer2/audio/Sonic.java | 7 ++++--- .../android/exoplayer2/audio/SonicAudioProcessor.java | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java index 0bf6baa4d0..6cd46bb705 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java @@ -30,6 +30,7 @@ import java.util.Arrays; private static final int MINIMUM_PITCH = 65; private static final int MAXIMUM_PITCH = 400; private static final int AMDF_FREQUENCY = 4000; + private static final int BYTES_PER_SAMPLE = 2; private final int inputSampleRateHz; private final int channelCount; @@ -157,9 +158,9 @@ import java.util.Arrays; maxDiff = 0; } - /** Returns the number of output frames that can be read with {@link #getOutput(ShortBuffer)}. */ - public int getFramesAvailable() { - return outputFrameCount; + /** Returns the size of output that can be read with {@link #getOutput(ShortBuffer)}, in bytes. */ + public int getOutputSize() { + return outputFrameCount * channelCount * BYTES_PER_SAMPLE; } // Internal methods. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java index 0d938d33f4..bd32e5ee6f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java @@ -210,7 +210,7 @@ public final class SonicAudioProcessor implements AudioProcessor { sonic.queueInput(shortBuffer); inputBuffer.position(inputBuffer.position() + inputSize); } - int outputSize = sonic.getFramesAvailable() * channelCount * 2; + int outputSize = sonic.getOutputSize(); if (outputSize > 0) { if (buffer.capacity() < outputSize) { buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder()); @@ -243,7 +243,7 @@ public final class SonicAudioProcessor implements AudioProcessor { @Override public boolean isEnded() { - return inputEnded && (sonic == null || sonic.getFramesAvailable() == 0); + return inputEnded && (sonic == null || sonic.getOutputSize() == 0); } @Override From 09835c454bfb9a5b542290a4028472bb1d53b378 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 Jul 2019 18:07:02 +0100 Subject: [PATCH 1488/1556] Bump version to 2.10.4 PiperOrigin-RevId: 260164426 --- RELEASENOTES.md | 24 +++++++++++-------- constants.gradle | 4 ++-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 ++--- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 176c786682..747436a69d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,7 +5,6 @@ * Add `PlaybackStatsListener` to collect `PlaybackStats` for playbacks analysis and analytics reporting (TODO: link to developer guide page/blog post). * Add basic DRM support to the Cast demo app. -* Offline: Add `Scheduler` implementation that uses `WorkManager`. * Assume that encrypted content requires secure decoders in renderer support checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Decoders: Prefer decoders that advertise format support over ones that do not, @@ -19,23 +18,28 @@ `SourceInfoRefreshListener` anymore. Instead make it accessible through `Player.getCurrentManifest()` and `Timeline.Window.manifest`. Also rename `SourceInfoRefreshListener` to `MediaSourceCaller`. -* Flac extension: Parse `VORBIS_COMMENT` metadata - ([#5527](https://github.com/google/ExoPlayer/issues/5527)). * Set `compileSdkVersion` to 29 to use Android Q APIs. * Add `enable` and `disable` methods to `MediaSource` to improve resource management in playlists. -* Fix issue where initial seek positions get ignored when playing a preroll ad. -* Fix `DataSchemeDataSource` re-opening and range requests - ([#6192](https://github.com/google/ExoPlayer/issues/6192)). +* Improve text selection logic to always prefer the better language matches + over other selection parameters. + +### 2.10.4 ### + +* Offline: Add `Scheduler` implementation that uses `WorkManager`. +* Add ability to specify a description when creating notification channels via + ExoPlayer library classes. * Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language tags instead of 3-letter ISO 639-2 language tags. +* Fix issue where initial seek positions get ignored when playing a preroll ad + ([#6201](https://github.com/google/ExoPlayer/issues/6201)). * Fix issue where invalid language tags were normalized to "und" instead of keeping the original ([#6153](https://github.com/google/ExoPlayer/issues/6153)). -* Add ability to specify a description when creating notification channels via - ExoPlayer library classes. -* Improve text selection logic to always prefer the better language matches - over other selection parameters. +* Fix `DataSchemeDataSource` re-opening and range requests + ([#6192](https://github.com/google/ExoPlayer/issues/6192)). +* Flac extension: Parse `VORBIS_COMMENT` metadata + ([#5527](https://github.com/google/ExoPlayer/issues/5527)). ### 2.10.3 ### diff --git a/constants.gradle b/constants.gradle index c8136ea471..aba52817bc 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.10.3' - releaseVersionCode = 2010003 + releaseVersion = '2.10.4' + releaseVersionCode = 2010004 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 29 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 190f4de5a6..f420f20767 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.10.3"; + public static final String VERSION = "2.10.4"; /** 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.10.3"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.4"; /** * 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 = 2010003; + public static final int VERSION_INT = 2010004; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From ea64ecf2c40854deed3120d2fb38b68f67ec516d Mon Sep 17 00:00:00 2001 From: "Venkatarama NG. Avadhani" Date: Mon, 29 Jul 2019 14:24:42 +0530 Subject: [PATCH 1489/1556] Parse Picture Metadata in FLAC --- .../exoplayer2/ext/flac/FlacExtractor.java | 4 +- extensions/flac/src/main/jni/flac_jni.cc | 58 ++++++- extensions/flac/src/main/jni/flac_parser.cc | 19 +++ .../flac/src/main/jni/include/flac_parser.h | 21 +++ .../metadata/flac/PictureFrame.java | 154 ++++++++++++++++++ .../{vorbis => flac}/VorbisComment.java | 2 +- .../exoplayer2/util/FlacStreamMetadata.java | 16 +- .../exoplayer2/metadata/flac/PictureTest.java | 43 +++++ .../{vorbis => flac}/VorbisCommentTest.java | 2 +- .../util/FlacStreamMetadataTest.java | 18 +- .../android/exoplayer2/ui/PlayerView.java | 22 ++- 11 files changed, 333 insertions(+), 26 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java rename library/core/src/main/java/com/google/android/exoplayer2/metadata/{vorbis => flac}/VorbisComment.java (97%) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureTest.java rename library/core/src/test/java/com/google/android/exoplayer2/metadata/{vorbis => flac}/VorbisCommentTest.java (96%) diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 151875c2c5..9f79f09117 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -229,8 +229,8 @@ public final class FlacExtractor implements Extractor { binarySearchSeeker = outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput); Metadata metadata = id3MetadataDisabled ? null : id3Metadata; - if (streamMetadata.vorbisComments != null) { - metadata = streamMetadata.vorbisComments.copyWithAppendedEntriesFrom(metadata); + if (streamMetadata.flacMetadata != null) { + metadata = streamMetadata.flacMetadata.copyWithAppendedEntriesFrom(metadata); } outputFormat(streamMetadata, metadata, trackOutput); outputBuffer.reset(streamMetadata.maxDecodedFrameSize()); diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 4ba071e1ca..4ccd24781b 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -102,10 +102,10 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { jmethodID arrayListConstructor = env->GetMethodID(arrayListClass, "", "()V"); jobject commentList = env->NewObject(arrayListClass, arrayListConstructor); + jmethodID arrayListAddMethod = + env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z"); if (context->parser->isVorbisCommentsValid()) { - jmethodID arrayListAddMethod = - env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z"); std::vector vorbisComments = context->parser->getVorbisComments(); for (std::vector::const_iterator vorbisComment = @@ -117,6 +117,39 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { } } + jobject pictureList = env->NewObject(arrayListClass, arrayListConstructor); + bool picValid = context->parser->isPicValid(); + if (picValid) { + std::vector pictures = context->parser->getPictures(); + jclass flacPictureFrameClass = env->FindClass( + "com/google/android/exoplayer2/metadata/flac/PictureFrame"); + jmethodID flacPictureFrameConstructor = env->GetMethodID( + flacPictureFrameClass, "", + "(ILjava/lang/String;Ljava/lang/String;IIII[B)V"); + for (std::vector::const_iterator picture = pictures.begin(); + picture != pictures.end(); ++picture) { + jstring mimeType = env->NewStringUTF(picture->mimeType.c_str()); + jstring description = env->NewStringUTF(picture->description.c_str()); + jbyteArray picArr = env->NewByteArray(picture->data.size()); + env->SetByteArrayRegion(picArr, 0, picture->data.size(), + (signed char *)&picture->data[0]); + jobject flacPictureFrame = env->NewObject(flacPictureFrameClass, + flacPictureFrameConstructor, + picture->type, + mimeType, + description, + picture->width, + picture->height, + picture->depth, + picture->colors, + picArr); + env->CallBooleanMethod(pictureList, arrayListAddMethod, flacPictureFrame); + env->DeleteLocalRef(mimeType); + env->DeleteLocalRef(description); + env->DeleteLocalRef(picArr); + } + } + const FLAC__StreamMetadata_StreamInfo &streamInfo = context->parser->getStreamInfo(); @@ -124,14 +157,21 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { "com/google/android/exoplayer2/util/" "FlacStreamMetadata"); jmethodID flacStreamMetadataConstructor = env->GetMethodID( - flacStreamMetadataClass, "", "(IIIIIIIJLjava/util/List;)V"); + flacStreamMetadataClass, "", + "(IIIIIIIJLjava/util/List;Ljava/util/List;)V"); - return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor, - streamInfo.min_blocksize, streamInfo.max_blocksize, - streamInfo.min_framesize, streamInfo.max_framesize, - streamInfo.sample_rate, streamInfo.channels, - streamInfo.bits_per_sample, streamInfo.total_samples, - commentList); + jobject streamMetaData = env->NewObject(flacStreamMetadataClass, + flacStreamMetadataConstructor, + streamInfo.min_blocksize, + streamInfo.max_blocksize, + streamInfo.min_framesize, + streamInfo.max_framesize, + streamInfo.sample_rate, + streamInfo.channels, + streamInfo.bits_per_sample, + streamInfo.total_samples, + commentList, pictureList); + return streamMetaData; } DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) { diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index b2d074252d..fafc254482 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -191,6 +191,22 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) { ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT"); } break; + case FLAC__METADATA_TYPE_PICTURE: + { + const FLAC__StreamMetadata_Picture *pic = &metadata->data.picture; + flacPicture picture; + picture.mimeType.assign(std::string(pic->mime_type)); + picture.description.assign(std::string((char *)pic->description)); + picture.data.assign(pic->data, pic->data + pic->data_length); + picture.width = pic->width; + picture.height = pic->height; + picture.depth = pic->depth; + picture.colors = pic->colors; + picture.type = pic->type; + mPictures.push_back(picture); + mPicValid = true; + break; + } default: ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type); break; @@ -253,6 +269,7 @@ FLACParser::FLACParser(DataSource *source) mEOF(false), mStreamInfoValid(false), mVorbisCommentsValid(false), + mPicValid(false), mWriteRequested(false), mWriteCompleted(false), mWriteBuffer(NULL), @@ -288,6 +305,8 @@ bool FLACParser::init() { FLAC__METADATA_TYPE_SEEKTABLE); FLAC__stream_decoder_set_metadata_respond(mDecoder, FLAC__METADATA_TYPE_VORBIS_COMMENT); + FLAC__stream_decoder_set_metadata_respond(mDecoder, + FLAC__METADATA_TYPE_PICTURE); FLAC__StreamDecoderInitStatus initStatus; initStatus = FLAC__stream_decoder_init_stream( mDecoder, read_callback, seek_callback, tell_callback, length_callback, diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index f09a22e951..f1d175b94f 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -30,6 +30,17 @@ typedef int status_t; +typedef struct { + int type; + std::string mimeType; + std::string description; + FLAC__uint32 width; + FLAC__uint32 height; + FLAC__uint32 depth; + FLAC__uint32 colors; + std::vector data; +} flacPicture; + class FLACParser { public: FLACParser(DataSource *source); @@ -54,6 +65,10 @@ class FLACParser { return mVorbisComments; } + bool isPicValid() const { return mPicValid; } + + const std::vector& getPictures() const { return mPictures; } + int64_t getLastFrameTimestamp() const { return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); } @@ -82,7 +97,9 @@ class FLACParser { if (newPosition == 0) { mStreamInfoValid = false; mVorbisCommentsValid = false; + mPicValid = false; mVorbisComments.clear(); + mPictures.clear(); FLAC__stream_decoder_reset(mDecoder); } else { FLAC__stream_decoder_flush(mDecoder); @@ -132,6 +149,10 @@ class FLACParser { std::vector mVorbisComments; bool mVorbisCommentsValid; + // cached when the PICTURE metadata is parsed by libFLAC + std::vector mPictures; + bool mPicValid; + // cached when a decoded PCM block is "written" by libFLAC parser bool mWriteRequested; bool mWriteCompleted; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java new file mode 100644 index 0000000000..fcf1fd6e58 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2019 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.flac; + +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.metadata.Metadata; +import java.util.Arrays; + +/** A Picture parsed in a FLAC file. */ +public final class PictureFrame implements Metadata.Entry { + + /** The type of the picture. */ + public final int pictureType; + /** The mime type of the picture. */ + public final String mimeType; + /** A description of the picture. */ + @Nullable public final String description; + /** The pixel width of the picture. */ + public final int width; + /** The pixel height of the picture. */ + public final int height; + /** The color depth of the picture in bits-per-pixel. */ + public final int depth; + /** + * For indexed-color pictures (e.g. GIF), the number of colors used, or 0 for non-indexed + * pictures. + */ + public final int colors; + /** The encoded picture data. */ + public final byte[] pictureData; + + public PictureFrame( + int pictureType, + String mimeType, + @Nullable String description, + int width, + int height, + int depth, + int colors, + byte[] pictureData) { + this.pictureType = pictureType; + this.mimeType = mimeType; + this.description = description; + this.width = width; + this.height = height; + this.depth = depth; + this.colors = colors; + this.pictureData = pictureData; + } + + /* package */ PictureFrame(Parcel in) { + this.pictureType = in.readInt(); + this.mimeType = castNonNull(in.readString()); + this.description = castNonNull(in.readString()); + this.width = in.readInt(); + this.height = in.readInt(); + this.depth = in.readInt(); + this.colors = in.readInt(); + this.pictureData = castNonNull(in.createByteArray()); + } + + @Override + public String toString() { + return "FLAC Picture" + + "\nType: " + pictureType + + "\nMime Type: " + mimeType + + "\nDescription: " + description + + "\nWidth: " + width + + "\nHeight: " + height + + "\nDepth: " + depth + + "\nColors: " + colors; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PictureFrame other = (PictureFrame) obj; + return (pictureType == other.pictureType) + && mimeType.equals(other.mimeType) + && description.equals(other.description) + && (width == other.width) + && (height == other.height) + && (depth == other.depth) + && (colors == other.colors) + && Arrays.equals(pictureData, other.pictureData); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + pictureType; + result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + width; + result = 31 * result + height; + result = 31 * result + depth; + result = 31 * result + colors; + result = 31 * result + Arrays.hashCode(pictureData); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(pictureType); + dest.writeString(mimeType); + dest.writeString(description); + dest.writeInt(width); + dest.writeInt(height); + dest.writeInt(depth); + dest.writeInt(colors); + dest.writeByteArray(pictureData); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public PictureFrame createFromParcel(Parcel in) { + return new PictureFrame(in); + } + + @Override + public PictureFrame[] newArray(int size) { + return new PictureFrame[size]; + } + }; +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisComment.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java similarity index 97% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisComment.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java index b1951cbc13..9f44cdf393 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisComment.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.metadata.vorbis; +package com.google.android.exoplayer2.metadata.flac; import static com.google.android.exoplayer2.util.Util.castNonNull; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java index 43fdda367e..48680b5095 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java @@ -18,7 +18,8 @@ package com.google.android.exoplayer2.util; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.vorbis.VorbisComment; +import com.google.android.exoplayer2.metadata.flac.PictureFrame; +import com.google.android.exoplayer2.metadata.flac.VorbisComment; import java.util.ArrayList; import java.util.List; @@ -35,7 +36,7 @@ public final class FlacStreamMetadata { public final int channels; public final int bitsPerSample; public final long totalSamples; - @Nullable public final Metadata vorbisComments; + @Nullable public final Metadata flacMetadata; private static final String SEPARATOR = "="; @@ -58,7 +59,7 @@ public final class FlacStreamMetadata { this.channels = scratch.readBits(3) + 1; this.bitsPerSample = scratch.readBits(5) + 1; this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL); - this.vorbisComments = null; + this.flacMetadata = null; } /** @@ -71,10 +72,13 @@ public final class FlacStreamMetadata { * @param bitsPerSample Number of bits per sample of the FLAC stream. * @param totalSamples Total samples of the FLAC stream. * @param vorbisComments Vorbis comments. Each entry must be in key=value form. + * @param pictureList A list of pictures in the stream. * @see FLAC format * METADATA_BLOCK_STREAMINFO * @see FLAC format * METADATA_BLOCK_VORBIS_COMMENT + * @see FLAC format + * METADATA_BLOCK_PICTURE */ public FlacStreamMetadata( int minBlockSize, @@ -85,7 +89,8 @@ public final class FlacStreamMetadata { int channels, int bitsPerSample, long totalSamples, - List vorbisComments) { + List vorbisComments, + List pictureList) { this.minBlockSize = minBlockSize; this.maxBlockSize = maxBlockSize; this.minFrameSize = minFrameSize; @@ -94,7 +99,8 @@ public final class FlacStreamMetadata { this.channels = channels; this.bitsPerSample = bitsPerSample; this.totalSamples = totalSamples; - this.vorbisComments = parseVorbisComments(vorbisComments); + Metadata metadata = new Metadata(pictureList); + this.flacMetadata = metadata.copyWithAppendedEntriesFrom(parseVorbisComments(vorbisComments)); } /** Returns the maximum size for a decoded frame from the FLAC stream. */ diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureTest.java new file mode 100644 index 0000000000..04a5b46e26 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2019 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.flac; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test for {@link PictureFrame}. */ +@RunWith(AndroidJUnit4.class) +public class PictureTest { + + @Test + public void testParcelable() { + PictureFrame pictureFrameToParcel = + new PictureFrame(0, "", "", 0, 0, 0, 0, new byte[0]); + + Parcel parcel = Parcel.obtain(); + pictureFrameToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + PictureFrame pictureFrameFromParcel = PictureFrame.CREATOR.createFromParcel(parcel); + assertThat(pictureFrameFromParcel).isEqualTo(pictureFrameToParcel); + + parcel.recycle(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java similarity index 96% rename from library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java index 868b28b0e1..bb118e381a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.metadata.vorbis; +package com.google.android.exoplayer2.metadata.flac; import static com.google.common.truth.Truth.assertThat; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java index 325d9b19f6..c556282ca2 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java @@ -19,7 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.vorbis.VorbisComment; +import com.google.android.exoplayer2.metadata.flac.VorbisComment; import java.util.ArrayList; import org.junit.Test; import org.junit.runner.RunWith; @@ -34,7 +34,9 @@ public final class FlacStreamMetadataTest { commentsList.add("Title=Song"); commentsList.add("Artist=Singer"); - Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()) + .flacMetadata; assertThat(metadata.length()).isEqualTo(2); VorbisComment commentFrame = (VorbisComment) metadata.get(0); @@ -49,7 +51,9 @@ public final class FlacStreamMetadataTest { public void parseEmptyVorbisComments() { ArrayList commentsList = new ArrayList<>(); - Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()) + .flacMetadata; assertThat(metadata).isNull(); } @@ -59,7 +63,9 @@ public final class FlacStreamMetadataTest { ArrayList commentsList = new ArrayList<>(); commentsList.add("Title=So=ng"); - Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()) + .flacMetadata; assertThat(metadata.length()).isEqualTo(1); VorbisComment commentFrame = (VorbisComment) metadata.get(0); @@ -73,7 +79,9 @@ public final class FlacStreamMetadataTest { commentsList.add("TitleSong"); commentsList.add("Artist=Singer"); - Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()) + .flacMetadata; assertThat(metadata.length()).isEqualTo(1); VorbisComment commentFrame = (VorbisComment) metadata.get(0); 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 269c48c282..4ac007fa55 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 @@ -49,6 +49,7 @@ import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.flac.PictureFrame; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsLoader; @@ -303,6 +304,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider private boolean controllerHideOnTouch; private int textureViewRotation; private boolean isTouching; + private static final int PICTURE_TYPE_FRONT_COVER = 3; public PlayerView(Context context) { this(context, null); @@ -1246,15 +1248,29 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } private boolean setArtworkFromMetadata(Metadata metadata) { + boolean isArtworkSet = false; + int currentPicType = -1; for (int i = 0; i < metadata.length(); i++) { Metadata.Entry metadataEntry = metadata.get(i); + int picType; + byte[] bitmapData; if (metadataEntry instanceof ApicFrame) { - byte[] bitmapData = ((ApicFrame) metadataEntry).pictureData; + bitmapData = ((ApicFrame) metadataEntry).pictureData; + picType = ((ApicFrame) metadataEntry).pictureType; + } else if (metadataEntry instanceof PictureFrame) { + bitmapData = ((PictureFrame) metadataEntry).pictureData; + picType = ((PictureFrame) metadataEntry).pictureType; + } else { + continue; + } + /* Prefers the first front cover picture in the picture list */ + if (currentPicType != PICTURE_TYPE_FRONT_COVER) { Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length); - return setDrawableArtwork(new BitmapDrawable(getResources(), bitmap)); + isArtworkSet = setDrawableArtwork(new BitmapDrawable(getResources(), bitmap)); + currentPicType = picType; } } - return false; + return isArtworkSet; } private boolean setDrawableArtwork(@Nullable Drawable drawable) { From 846e0666dfe3de143668fcbfe932a14995893763 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 29 Jul 2019 11:06:24 +0100 Subject: [PATCH 1490/1556] Restructure updatePeriods code for better readability. Moved update of reading and playing periods in their own respective method. This is a no-op change. PiperOrigin-RevId: 260463668 --- .../exoplayer2/ExoPlayerImplInternal.java | 168 ++++++++++-------- .../android/exoplayer2/MediaPeriodQueue.java | 5 + 2 files changed, 101 insertions(+), 72 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 738a30fad1..d356fe11d2 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 @@ -1490,82 +1490,88 @@ import java.util.concurrent.atomic.AtomicBoolean; mediaSource.maybeThrowSourceInfoRefreshError(); return; } - - // Update the loading period if required. maybeUpdateLoadingPeriod(); + maybeUpdatePlayingPeriod(); + maybeUpdateReadingPeriod(); + } + + private void maybeUpdateLoadingPeriod() throws IOException { + queue.reevaluateBuffer(rendererPositionUs); + if (queue.shouldLoadNextMediaPeriod()) { + MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo); + if (info == null) { + maybeThrowSourceInfoRefreshError(); + } else { + MediaPeriod mediaPeriod = + queue.enqueueNextMediaPeriod( + rendererCapabilities, trackSelector, loadControl.getAllocator(), mediaSource, info); + mediaPeriod.prepare(this, info.startPositionUs); + setIsLoading(true); + handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); + } + } MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { setIsLoading(false); } else if (!playbackInfo.isLoading) { maybeContinueLoading(); } + } - if (!queue.hasPlayingPeriod()) { - // We're waiting for the first period to be prepared. - return; - } - - // Advance the playing period if necessary. - MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); - MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + private void maybeUpdatePlayingPeriod() throws ExoPlaybackException { boolean advancedPlayingPeriod = false; - while (playWhenReady - && playingPeriodHolder != readingPeriodHolder - && rendererPositionUs >= playingPeriodHolder.getNext().getStartPositionRendererTime()) { - // All enabled renderers' streams have been read to the end, and the playback position reached - // the end of the playing period, so advance playback to the next period. + while (shouldAdvancePlayingPeriod()) { if (advancedPlayingPeriod) { // If we advance more than one period at a time, notify listeners after each update. maybeNotifyPlaybackInfoChanged(); } - int discontinuityReason = - playingPeriodHolder.info.isLastInTimelinePeriod - ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION - : Player.DISCONTINUITY_REASON_AD_INSERTION; - MediaPeriodHolder oldPlayingPeriodHolder = playingPeriodHolder; - playingPeriodHolder = queue.advancePlayingPeriod(); + MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); + MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod(); updatePlayingPeriodRenderers(oldPlayingPeriodHolder); playbackInfo = playbackInfo.copyWithNewPosition( - playingPeriodHolder.info.id, - playingPeriodHolder.info.startPositionUs, - playingPeriodHolder.info.contentPositionUs, + newPlayingPeriodHolder.info.id, + newPlayingPeriodHolder.info.startPositionUs, + newPlayingPeriodHolder.info.contentPositionUs, getTotalBufferedDurationUs()); + int discontinuityReason = + oldPlayingPeriodHolder.info.isLastInTimelinePeriod + ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION + : Player.DISCONTINUITY_REASON_AD_INSERTION; playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason); updatePlaybackPositions(); advancedPlayingPeriod = true; } + } - if (readingPeriodHolder.info.isFinal) { - for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; - // Defer setting the stream as final until the renderer has actually consumed the whole - // stream in case of playlist changes that cause the stream to be no longer final. - if (sampleStream != null && renderer.getStream() == sampleStream - && renderer.hasReadStreamToEnd()) { - renderer.setCurrentStreamFinal(); + private void maybeUpdateReadingPeriod() throws ExoPlaybackException, IOException { + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + if (readingPeriodHolder == null) { + return; + } + + if (readingPeriodHolder.getNext() == null) { + // We don't have a successor to advance the reading period to. + if (readingPeriodHolder.info.isFinal) { + for (int i = 0; i < renderers.length; i++) { + Renderer renderer = renderers[i]; + SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; + // Defer setting the stream as final until the renderer has actually consumed the whole + // stream in case of playlist changes that cause the stream to be no longer final. + if (sampleStream != null + && renderer.getStream() == sampleStream + && renderer.hasReadStreamToEnd()) { + renderer.setCurrentStreamFinal(); + } } } return; } - // Advance the reading period if necessary. - if (readingPeriodHolder.getNext() == null) { - // We don't have a successor to advance the reading period to. + if (!hasReadingPeriodFinishedReading()) { return; } - for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; - if (renderer.getStream() != sampleStream - || (sampleStream != null && !renderer.hasReadStreamToEnd())) { - // The current reading period is still being read by at least one renderer. - return; - } - } - if (!readingPeriodHolder.getNext().prepared) { // The successor is not prepared yet. maybeThrowPeriodPrepareError(); @@ -1576,18 +1582,18 @@ import java.util.concurrent.atomic.AtomicBoolean; readingPeriodHolder = queue.advanceReadingPeriod(); TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult(); - boolean initialDiscontinuity = - readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET; + if (readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET) { + // The new period starts with a discontinuity, so the renderers will play out all data, then + // be disabled and re-enabled when they start playing the next period. + setAllRendererStreamsFinal(); + return; + } for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; boolean rendererWasEnabled = oldTrackSelectorResult.isRendererEnabled(i); - if (!rendererWasEnabled) { - // The renderer was disabled and will be enabled when we play the next period. - } else if (initialDiscontinuity) { - // The new period starts with a discontinuity, so the renderer will play out all data then - // be disabled and re-enabled when it starts playing the next period. - renderer.setCurrentStreamFinal(); - } else if (!renderer.isCurrentStreamFinal()) { + if (rendererWasEnabled && !renderer.isCurrentStreamFinal()) { + // The renderer is enabled and its stream is not final, so we still have a chance to replace + // the sample streams. TrackSelection newSelection = newTrackSelectorResult.selections.get(i); boolean newRendererEnabled = newTrackSelectorResult.isRendererEnabled(i); boolean isNoSampleRenderer = rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE; @@ -1615,23 +1621,41 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private void maybeUpdateLoadingPeriod() throws IOException { - queue.reevaluateBuffer(rendererPositionUs); - if (queue.shouldLoadNextMediaPeriod()) { - MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo); - if (info == null) { - maybeThrowSourceInfoRefreshError(); - } else { - MediaPeriod mediaPeriod = - queue.enqueueNextMediaPeriod( - rendererCapabilities, - trackSelector, - loadControl.getAllocator(), - mediaSource, - info); - mediaPeriod.prepare(this, info.startPositionUs); - setIsLoading(true); - handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); + private boolean shouldAdvancePlayingPeriod() { + if (!playWhenReady) { + return false; + } + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + if (playingPeriodHolder == null) { + return false; + } + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + if (playingPeriodHolder == readingPeriodHolder) { + return false; + } + MediaPeriodHolder nextPlayingPeriodHolder = + Assertions.checkNotNull(playingPeriodHolder.getNext()); + return rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime(); + } + + private boolean hasReadingPeriodFinishedReading() { + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + for (int i = 0; i < renderers.length; i++) { + Renderer renderer = renderers[i]; + SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; + if (renderer.getStream() != sampleStream + || (sampleStream != null && !renderer.hasReadStreamToEnd())) { + // The current reading period is still being read by at least one renderer. + return false; + } + } + return true; + } + + private void setAllRendererStreamsFinal() { + for (Renderer renderer : renderers) { + if (renderer.getStream() != null) { + renderer.setCurrentStreamFinal(); } } } 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 2597cd9b3f..9c0dd80a10 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 @@ -170,6 +170,7 @@ import com.google.android.exoplayer2.util.Assertions; * Returns the loading period holder which is at the end of the queue, or null if the queue is * empty. */ + @Nullable public MediaPeriodHolder getLoadingPeriod() { return loading; } @@ -178,6 +179,7 @@ import com.google.android.exoplayer2.util.Assertions; * Returns the playing period holder which is at the front of the queue, or null if the queue is * empty or hasn't started playing. */ + @Nullable public MediaPeriodHolder getPlayingPeriod() { return playing; } @@ -186,6 +188,7 @@ import com.google.android.exoplayer2.util.Assertions; * Returns the reading period holder, or null if the queue is empty or the player hasn't started * reading. */ + @Nullable public MediaPeriodHolder getReadingPeriod() { return reading; } @@ -194,6 +197,7 @@ import com.google.android.exoplayer2.util.Assertions; * Returns the period holder in the front of the queue which is the playing period holder when * playing, or null if the queue is empty. */ + @Nullable public MediaPeriodHolder getFrontPeriod() { return hasPlayingPeriod() ? playing : loading; } @@ -221,6 +225,7 @@ import com.google.android.exoplayer2.util.Assertions; * * @return The updated playing period holder, or null if the queue is or becomes empty. */ + @Nullable public MediaPeriodHolder advancePlayingPeriod() { if (playing != null) { if (playing == reading) { From 7703676c87a220277dbb89f80a53e164e01451cb Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 29 Jul 2019 11:06:50 +0100 Subject: [PATCH 1491/1556] Swap reading and playing media period updates. Both periods are rarely updated in the same iteration. If they are, advancing the reading period first seems more logical and also more efficient as it may avoid one extra doSomeWork iteration. PiperOrigin-RevId: 260463735 --- .../exoplayer2/ExoPlayerImplInternal.java | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 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 d356fe11d2..b6317941fb 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 @@ -1491,8 +1491,8 @@ import java.util.concurrent.atomic.AtomicBoolean; return; } maybeUpdateLoadingPeriod(); - maybeUpdatePlayingPeriod(); maybeUpdateReadingPeriod(); + maybeUpdatePlayingPeriod(); } private void maybeUpdateLoadingPeriod() throws IOException { @@ -1518,32 +1518,6 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private void maybeUpdatePlayingPeriod() throws ExoPlaybackException { - boolean advancedPlayingPeriod = false; - while (shouldAdvancePlayingPeriod()) { - if (advancedPlayingPeriod) { - // If we advance more than one period at a time, notify listeners after each update. - maybeNotifyPlaybackInfoChanged(); - } - MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); - MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod(); - updatePlayingPeriodRenderers(oldPlayingPeriodHolder); - playbackInfo = - playbackInfo.copyWithNewPosition( - newPlayingPeriodHolder.info.id, - newPlayingPeriodHolder.info.startPositionUs, - newPlayingPeriodHolder.info.contentPositionUs, - getTotalBufferedDurationUs()); - int discontinuityReason = - oldPlayingPeriodHolder.info.isLastInTimelinePeriod - ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION - : Player.DISCONTINUITY_REASON_AD_INSERTION; - playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason); - updatePlaybackPositions(); - advancedPlayingPeriod = true; - } - } - private void maybeUpdateReadingPeriod() throws ExoPlaybackException, IOException { MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); if (readingPeriodHolder == null) { @@ -1621,6 +1595,32 @@ import java.util.concurrent.atomic.AtomicBoolean; } } + private void maybeUpdatePlayingPeriod() throws ExoPlaybackException { + boolean advancedPlayingPeriod = false; + while (shouldAdvancePlayingPeriod()) { + if (advancedPlayingPeriod) { + // If we advance more than one period at a time, notify listeners after each update. + maybeNotifyPlaybackInfoChanged(); + } + MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); + MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod(); + updatePlayingPeriodRenderers(oldPlayingPeriodHolder); + playbackInfo = + playbackInfo.copyWithNewPosition( + newPlayingPeriodHolder.info.id, + newPlayingPeriodHolder.info.startPositionUs, + newPlayingPeriodHolder.info.contentPositionUs, + getTotalBufferedDurationUs()); + int discontinuityReason = + oldPlayingPeriodHolder.info.isLastInTimelinePeriod + ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION + : Player.DISCONTINUITY_REASON_AD_INSERTION; + playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason); + updatePlaybackPositions(); + advancedPlayingPeriod = true; + } + } + private boolean shouldAdvancePlayingPeriod() { if (!playWhenReady) { return false; From d77d661e5283cb3ae55cbc2f29098e2f964e30b5 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 29 Jul 2019 13:41:48 +0100 Subject: [PATCH 1492/1556] Default viewport constraints to match primary display PiperOrigin-RevId: 260479923 --- RELEASENOTES.md | 2 + .../exoplayer2/castdemo/PlayerManager.java | 7 +- .../exoplayer2/gvrdemo/PlayerActivity.java | 3 +- .../exoplayer2/demo/DownloadTracker.java | 19 +-- .../exoplayer2/demo/PlayerActivity.java | 4 +- .../exoplayer2/ext/flac/FlacPlaybackTest.java | 2 +- .../exoplayer2/ext/opus/OpusPlaybackTest.java | 2 +- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 2 +- .../android/exoplayer2/ExoPlayerFactory.java | 2 +- .../exoplayer2/offline/DownloadHelper.java | 121 +++++++++++++++--- .../trackselection/DefaultTrackSelector.java | 76 +++++++++-- .../offline/DownloadHelperTest.java | 10 +- .../DefaultTrackSelectorTest.java | 119 ++++++++--------- .../dash/offline/DownloadHelperTest.java | 4 +- .../hls/offline/DownloadHelperTest.java | 4 +- .../offline/DownloadHelperTest.java | 4 +- .../playbacktests/gts/DashTestRunner.java | 4 +- .../exoplayer2/testutil/ExoHostedTest.java | 3 +- .../testutil/ExoPlayerTestRunner.java | 2 +- 19 files changed, 259 insertions(+), 131 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 747436a69d..7afe0a78cd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* Update `DefaultTrackSelector` to apply a viewport constraint for the default + display by default. * Add `PlaybackStatsListener` to collect `PlaybackStats` for playbacks analysis and analytics reporting (TODO: link to developer guide page/blog post). * Add basic DRM support to the Cast demo app. 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 b877ac7593..421269772c 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 @@ -21,13 +21,11 @@ import androidx.annotation.Nullable; import android.view.KeyEvent; import android.view.View; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.Player.TimelineChangeReason; -import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; @@ -40,7 +38,6 @@ import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; @@ -99,9 +96,7 @@ import org.json.JSONObject; currentItemIndex = C.INDEX_UNSET; concatenatingMediaSource = new ConcatenatingMediaSource(); - DefaultTrackSelector trackSelector = new DefaultTrackSelector(); - RenderersFactory renderersFactory = new DefaultRenderersFactory(context); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector); + exoPlayer = ExoPlayerFactory.newSimpleInstance(context); exoPlayer.addListener(this); localPlayerView.setPlayer(exoPlayer); diff --git a/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/PlayerActivity.java b/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/PlayerActivity.java index bd9c85da51..059f26b374 100644 --- a/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/PlayerActivity.java +++ b/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/PlayerActivity.java @@ -35,7 +35,6 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -135,7 +134,7 @@ public class PlayerActivity extends GvrPlayerActivity implements PlaybackPrepare DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this); - trackSelector = new DefaultTrackSelector(new AdaptiveTrackSelection.Factory()); + trackSelector = new DefaultTrackSelector(/* context= */ this); lastSeenTrackGroupArray = null; player = diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java index e1e866bbee..839ed304bd 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.offline.DownloadIndex; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloadService; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Log; @@ -55,6 +56,7 @@ public class DownloadTracker { private final CopyOnWriteArraySet listeners; private final HashMap downloads; private final DownloadIndex downloadIndex; + private final DefaultTrackSelector.Parameters trackSelectorParameters; @Nullable private StartDownloadDialogHelper startDownloadDialogHelper; @@ -65,6 +67,7 @@ public class DownloadTracker { listeners = new CopyOnWriteArraySet<>(); downloads = new HashMap<>(); downloadIndex = downloadManager.getDownloadIndex(); + trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context); downloadManager.addListener(new DownloadManagerListener()); loadDownloads(); } @@ -123,13 +126,13 @@ public class DownloadTracker { int type = Util.inferContentType(uri, extension); switch (type) { case C.TYPE_DASH: - return DownloadHelper.forDash(uri, dataSourceFactory, renderersFactory); + return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory); case C.TYPE_SS: - return DownloadHelper.forSmoothStreaming(uri, dataSourceFactory, renderersFactory); + return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory); case C.TYPE_HLS: - return DownloadHelper.forHls(uri, dataSourceFactory, renderersFactory); + return DownloadHelper.forHls(context, uri, dataSourceFactory, renderersFactory); case C.TYPE_OTHER: - return DownloadHelper.forProgressive(uri); + return DownloadHelper.forProgressive(context, uri); default: throw new IllegalStateException("Unsupported type: " + type); } @@ -202,7 +205,7 @@ public class DownloadTracker { TrackSelectionDialog.createForMappedTrackInfoAndParameters( /* titleId= */ R.string.exo_download_description, mappedTrackInfo, - /* initialParameters= */ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, + trackSelectorParameters, /* allowAdaptiveSelections =*/ false, /* allowMultipleOverrides= */ true, /* onClickListener= */ this, @@ -212,9 +215,7 @@ public class DownloadTracker { @Override public void onPrepareError(DownloadHelper helper, IOException e) { - Toast.makeText( - context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG) - .show(); + Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show(); Log.e(TAG, "Failed to start download", e); } @@ -229,7 +230,7 @@ public class DownloadTracker { downloadHelper.addTrackSelectionForSingleRenderer( periodIndex, /* rendererIndex= */ i, - DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, + trackSelectorParameters, trackSelectionDialog.getOverrides(/* rendererIndex= */ i)); } } 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 40b1a94991..d8bfe23674 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 @@ -195,7 +195,7 @@ public class PlayerActivity extends AppCompatActivity startWindow = savedInstanceState.getInt(KEY_WINDOW); startPosition = savedInstanceState.getLong(KEY_POSITION); } else { - trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build(); + trackSelectorParameters = DefaultTrackSelector.Parameters.getDefaults(/* context= */ this); clearStartPosition(); } } @@ -411,7 +411,7 @@ public class PlayerActivity extends AppCompatActivity RenderersFactory renderersFactory = ((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders); - trackSelector = new DefaultTrackSelector(trackSelectionFactory); + trackSelector = new DefaultTrackSelector(/* context= */ this, trackSelectionFactory); trackSelector.setParameters(trackSelectorParameters); lastSeenTrackGroupArray = null; diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java index 12ef68ee3c..c10d6fdb27 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java @@ -82,7 +82,7 @@ public class FlacPlaybackTest { public void run() { Looper.prepare(); LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer(); - DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + DefaultTrackSelector trackSelector = new DefaultTrackSelector(context); player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); MediaSource mediaSource = diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java index 7c6835db0b..382ee38e06 100644 --- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java +++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java @@ -82,7 +82,7 @@ public class OpusPlaybackTest { public void run() { Looper.prepare(); LibopusAudioRenderer audioRenderer = new LibopusAudioRenderer(); - DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + DefaultTrackSelector trackSelector = new DefaultTrackSelector(context); player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); MediaSource mediaSource = diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index 5ebeca68d0..9be1d9c0e5 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -115,7 +115,7 @@ public class VpxPlaybackTest { public void run() { Looper.prepare(); LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(0); - DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + DefaultTrackSelector trackSelector = new DefaultTrackSelector(context); player = ExoPlayerFactory.newInstance(context, new Renderer[] {videoRenderer}, trackSelector); player.addListener(this); MediaSource mediaSource = diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index 59647feaa9..956f22f719 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -102,7 +102,7 @@ public final class ExoPlayerFactory { * @param context A {@link Context}. */ public static SimpleExoPlayer newSimpleInstance(Context context) { - return newSimpleInstance(context, new DefaultTrackSelector()); + return newSimpleInstance(context, new DefaultTrackSelector(context)); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 4b5bf3c8a4..6952413129 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.offline; +import android.content.Context; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; @@ -82,12 +83,25 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; */ public final class DownloadHelper { + /** Default track selection parameters for downloading, but without any viewport constraints. */ + public static final Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT = + Parameters.DEFAULT_WITHOUT_VIEWPORT.buildUpon().setForceHighestSupportedBitrate(true).build(); + /** - * The default parameters used for track selection for downloading. This default selects the - * highest bitrate audio and video tracks which are supported by the renderers. + * @deprecated This instance does not have viewport constraints configured for the primary + * display. Use {@link #getDefaultTrackSelectorParameters(Context)} instead. */ + @Deprecated public static final DefaultTrackSelector.Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS = - new DefaultTrackSelector.ParametersBuilder().setForceHighestSupportedBitrate(true).build(); + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT; + + /** Returns the default parameters used for track selection for downloading. */ + public static DefaultTrackSelector.Parameters getDefaultTrackSelectorParameters(Context context) { + return Parameters.getDefaults(context) + .buildUpon() + .setForceHighestSupportedBitrate(true) + .build(); + } /** A callback to be notified when the {@link DownloadHelper} is prepared. */ public interface Callback { @@ -120,12 +134,9 @@ public final class DownloadHelper { private static final Constructor HLS_FACTORY_CONSTRUCTOR = getConstructor("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); - /** - * Creates a {@link DownloadHelper} for progressive streams. - * - * @param uri A stream {@link Uri}. - * @return A {@link DownloadHelper} for progressive streams. - */ + /** @deprecated Use {@link #forProgressive(Context, Uri)} */ + @Deprecated + @SuppressWarnings("deprecation") public static DownloadHelper forProgressive(Uri uri) { return forProgressive(uri, /* cacheKey= */ null); } @@ -133,23 +144,60 @@ public final class DownloadHelper { /** * Creates a {@link DownloadHelper} for progressive streams. * + * @param context Any {@link Context}. * @param uri A stream {@link Uri}. - * @param cacheKey An optional cache key. * @return A {@link DownloadHelper} for progressive streams. */ + public static DownloadHelper forProgressive(Context context, Uri uri) { + return forProgressive(context, uri, /* cacheKey= */ null); + } + + /** @deprecated Use {@link #forProgressive(Context, Uri, String)} */ + @Deprecated public static DownloadHelper forProgressive(Uri uri, @Nullable String cacheKey) { return new DownloadHelper( DownloadRequest.TYPE_PROGRESSIVE, uri, cacheKey, /* mediaSource= */ null, - DEFAULT_TRACK_SELECTOR_PARAMETERS, + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT, /* rendererCapabilities= */ new RendererCapabilities[0]); } + /** + * Creates a {@link DownloadHelper} for progressive streams. + * + * @param context Any {@link Context}. + * @param uri A stream {@link Uri}. + * @param cacheKey An optional cache key. + * @return A {@link DownloadHelper} for progressive streams. + */ + public static DownloadHelper forProgressive(Context context, Uri uri, @Nullable String cacheKey) { + return new DownloadHelper( + DownloadRequest.TYPE_PROGRESSIVE, + uri, + cacheKey, + /* mediaSource= */ null, + getDefaultTrackSelectorParameters(context), + /* rendererCapabilities= */ new RendererCapabilities[0]); + } + + /** @deprecated Use {@link #forDash(Context, Uri, Factory, RenderersFactory)} */ + @Deprecated + public static DownloadHelper forDash( + Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + return forDash( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); + } + /** * Creates a {@link DownloadHelper} for DASH streams. * + * @param context Any {@link Context}. * @param uri A manifest {@link Uri}. * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are @@ -158,13 +206,16 @@ public final class DownloadHelper { * @throws IllegalStateException If the DASH module is missing. */ public static DownloadHelper forDash( - Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + Context context, + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory) { return forDash( uri, dataSourceFactory, renderersFactory, /* drmSessionManager= */ null, - DEFAULT_TRACK_SELECTOR_PARAMETERS); + getDefaultTrackSelectorParameters(context)); } /** @@ -197,9 +248,22 @@ public final class DownloadHelper { Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } + /** @deprecated Use {@link #forHls(Context, Uri, Factory, RenderersFactory)} */ + @Deprecated + public static DownloadHelper forHls( + Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + return forHls( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); + } + /** * Creates a {@link DownloadHelper} for HLS streams. * + * @param context Any {@link Context}. * @param uri A playlist {@link Uri}. * @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are @@ -208,13 +272,16 @@ public final class DownloadHelper { * @throws IllegalStateException If the HLS module is missing. */ public static DownloadHelper forHls( - Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + Context context, + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory) { return forHls( uri, dataSourceFactory, renderersFactory, /* drmSessionManager= */ null, - DEFAULT_TRACK_SELECTOR_PARAMETERS); + getDefaultTrackSelectorParameters(context)); } /** @@ -247,9 +314,22 @@ public final class DownloadHelper { Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } + /** @deprecated Use {@link #forSmoothStreaming(Context, Uri, Factory, RenderersFactory)} */ + @Deprecated + public static DownloadHelper forSmoothStreaming( + Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + return forSmoothStreaming( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); + } + /** * Creates a {@link DownloadHelper} for SmoothStreaming streams. * + * @param context Any {@link Context}. * @param uri A manifest {@link Uri}. * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are @@ -258,13 +338,16 @@ public final class DownloadHelper { * @throws IllegalStateException If the SmoothStreaming module is missing. */ public static DownloadHelper forSmoothStreaming( - Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + Context context, + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory) { return forSmoothStreaming( uri, dataSourceFactory, renderersFactory, /* drmSessionManager= */ null, - DEFAULT_TRACK_SELECTOR_PARAMETERS); + getDefaultTrackSelectorParameters(context)); } /** @@ -370,10 +453,10 @@ public final class DownloadHelper { this.uri = uri; this.cacheKey = cacheKey; this.mediaSource = mediaSource; - this.trackSelector = new DefaultTrackSelector(new DownloadTrackSelection.Factory()); + this.trackSelector = + new DefaultTrackSelector(trackSelectorParameters, new DownloadTrackSelection.Factory()); this.rendererCapabilities = rendererCapabilities; this.scratchSet = new SparseIntArray(); - trackSelector.setParameters(trackSelectorParameters); trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter()); callbackHandler = new Handler(Util.getLooper()); window = new Timeline.Window(); 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 56eebfbee4..cc1742bb31 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 @@ -186,9 +186,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final SparseArray> selectionOverrides; private final SparseBooleanArray rendererDisabledFlags; - /** Creates a builder with default initial values. */ + /** + * @deprecated Initial viewport constraints will not be set based on the primary display when + * using this constructor. Use {@link #ParametersBuilder(Context)} instead. + */ + @Deprecated public ParametersBuilder() { - this(Parameters.DEFAULT); + this(Parameters.DEFAULT_WITHOUT_VIEWPORT); + } + + /** + * Creates a builder with default initial values. + * + * @param context Any context. + */ + public ParametersBuilder(Context context) { + this(Parameters.getDefaults(context)); } /** @@ -656,8 +669,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public static final class Parameters extends TrackSelectionParameters { - /** An instance with default values. */ - public static final Parameters DEFAULT = new Parameters(); + /** An instance with default values, except without any viewport constraints. */ + public static final Parameters DEFAULT_WITHOUT_VIEWPORT = new Parameters(); + + /** + * @deprecated This instance does not have viewport constraints configured for the primary + * display. Use {@link #getDefaults(Context)} instead. + */ + @Deprecated public static final Parameters DEFAULT = DEFAULT_WITHOUT_VIEWPORT; + + /** Returns an instance configured with default values. */ + public static Parameters getDefaults(Context context) { + return DEFAULT_WITHOUT_VIEWPORT + .buildUpon() + .setViewportSizeToPhysicalDisplaySize(context, /* viewportOrientationMayChange= */ true) + .build(); + } // Video /** @@ -707,14 +734,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { public final boolean allowVideoNonSeamlessAdaptiveness; /** * 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). + * tracks suitable for the viewport are selected. The default value is the physical width of the + * primary display, in pixels. */ public final int viewportWidth; /** * 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). + * only tracks suitable for the viewport are selected. The default value is the physical height + * of the primary display, in pixels. */ public final int viewportHeight; /** @@ -1284,13 +1311,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { private boolean allowMultipleAdaptiveSelections; + /** @deprecated Use {@link #DefaultTrackSelector(Context)} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public DefaultTrackSelector() { this(new AdaptiveTrackSelection.Factory()); } /** - * @deprecated Use {@link #DefaultTrackSelector()} instead. Custom bandwidth meter should be - * directly passed to the player in {@link ExoPlayerFactory}. + * @deprecated Use {@link #DefaultTrackSelector(Context)} instead. The bandwidth meter should be + * passed directly to the player in {@link ExoPlayerFactory}. */ @Deprecated @SuppressWarnings("deprecation") @@ -1298,10 +1328,32 @@ public class DefaultTrackSelector extends MappingTrackSelector { this(new AdaptiveTrackSelection.Factory(bandwidthMeter)); } - /** @param trackSelectionFactory A factory for {@link TrackSelection}s. */ + /** @deprecated Use {@link #DefaultTrackSelector(Context, TrackSelection.Factory)}. */ + @Deprecated public DefaultTrackSelector(TrackSelection.Factory trackSelectionFactory) { + this(Parameters.DEFAULT_WITHOUT_VIEWPORT, trackSelectionFactory); + } + + /** @param context Any {@link Context}. */ + public DefaultTrackSelector(Context context) { + this(context, new AdaptiveTrackSelection.Factory()); + } + + /** + * @param context Any {@link Context}. + * @param trackSelectionFactory A factory for {@link TrackSelection}s. + */ + public DefaultTrackSelector(Context context, TrackSelection.Factory trackSelectionFactory) { + this(Parameters.getDefaults(context), trackSelectionFactory); + } + + /** + * @param parameters Initial {@link Parameters}. + * @param trackSelectionFactory A factory for {@link TrackSelection}s. + */ + public DefaultTrackSelector(Parameters parameters, TrackSelection.Factory trackSelectionFactory) { this.trackSelectionFactory = trackSelectionFactory; - parametersReference = new AtomicReference<>(Parameters.DEFAULT); + parametersReference = new AtomicReference<>(parameters); } /** diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java index 479936b82f..111edc7af8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.robolectric.shadows.ShadowBaseLooper.shadowMainLooper; import android.net.Uri; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -36,7 +37,6 @@ import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.ParametersBuilder; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; @@ -113,7 +113,7 @@ public class DownloadHelperTest { testUri, TEST_CACHE_KEY, new TestMediaSource(), - DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT, Util.getRendererCapabilities(renderersFactory, /* drmSessionManager= */ null)); } @@ -244,7 +244,7 @@ public class DownloadHelperTest { throws Exception { prepareDownloadHelper(downloadHelper); DefaultTrackSelector.Parameters parameters = - new ParametersBuilder() + new DefaultTrackSelector.ParametersBuilder(ApplicationProvider.getApplicationContext()) .setPreferredAudioLanguage("ZH") .setPreferredTextLanguage("ZH") .setRendererDisabled(/* rendererIndex= */ 2, true) @@ -281,7 +281,7 @@ public class DownloadHelperTest { // Select parameters to require some merging of track groups because the new parameters add // all video tracks to initial video single track selection. DefaultTrackSelector.Parameters parameters = - new ParametersBuilder() + new DefaultTrackSelector.ParametersBuilder(ApplicationProvider.getApplicationContext()) .setPreferredAudioLanguage("ZH") .setPreferredTextLanguage("US") .build(); @@ -385,7 +385,7 @@ public class DownloadHelperTest { // Ensure we have track groups with multiple indices, renderers with multiple track groups and // also renderers without any track groups. DefaultTrackSelector.Parameters parameters = - new ParametersBuilder() + new DefaultTrackSelector.ParametersBuilder(ApplicationProvider.getApplicationContext()) .setPreferredAudioLanguage("ZH") .setPreferredTextLanguage("US") .build(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index e450175524..4622dc1734 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -27,9 +27,11 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; +import android.content.Context; import android.os.Parcel; import android.util.SparseArray; import android.util.SparseBooleanArray; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -97,6 +99,7 @@ public final class DefaultTrackSelectorTest { @Mock private InvalidationListener invalidationListener; @Mock private BandwidthMeter bandwidthMeter; + private Parameters defaultParameters; private DefaultTrackSelector trackSelector; @BeforeClass @@ -108,7 +111,9 @@ public final class DefaultTrackSelectorTest { public void setUp() { initMocks(this); when(bandwidthMeter.getBitrateEstimate()).thenReturn(1000000L); - trackSelector = new DefaultTrackSelector(); + Context context = ApplicationProvider.getApplicationContext(); + defaultParameters = Parameters.getDefaults(context); + trackSelector = new DefaultTrackSelector(context); trackSelector.init(invalidationListener, bandwidthMeter); } @@ -234,7 +239,7 @@ public final class DefaultTrackSelectorTest { /** Tests disabling a renderer. */ @Test public void testSelectTracksWithDisabledRenderer() throws ExoPlaybackException { - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setRendererDisabled(1, true)); + trackSelector.setParameters(defaultParameters.buildUpon().setRendererDisabled(1, true)); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS, periodId, TIMELINE); assertSelections(result, new TrackSelection[] {TRACK_SELECTIONS[0], null}); @@ -271,7 +276,7 @@ public final class DefaultTrackSelectorTest { /** Tests disabling a no-sample renderer. */ @Test public void testSelectTracksWithDisabledNoSampleRenderer() throws ExoPlaybackException { - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setRendererDisabled(1, true)); + trackSelector.setParameters(defaultParameters.buildUpon().setRendererDisabled(1, true)); TrackSelectorResult result = trackSelector.selectTracks( RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS, periodId, TIMELINE); @@ -281,14 +286,13 @@ public final class DefaultTrackSelectorTest { } /** - * Tests that track selector will not call - * {@link InvalidationListener#onTrackSelectionsInvalidated()} when it's set with default - * values of {@link Parameters}. + * Tests that track selector will not call {@link + * InvalidationListener#onTrackSelectionsInvalidated()} when it's set with default values of + * {@link Parameters}. */ @Test - public void testSetParameterWithDefaultParametersDoesNotNotifyInvalidationListener() - throws Exception { - trackSelector.setParameters(Parameters.DEFAULT); + public void testSetParameterWithDefaultParametersDoesNotNotifyInvalidationListener() { + trackSelector.setParameters(defaultParameters); verify(invalidationListener, never()).onTrackSelectionsInvalidated(); } @@ -297,24 +301,22 @@ public final class DefaultTrackSelectorTest { * when it's set with non-default values of {@link Parameters}. */ @Test - public void testSetParameterWithNonDefaultParameterNotifyInvalidationListener() - throws Exception { - Parameters parameters = Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage("eng").build(); - trackSelector.setParameters(parameters); + public void testSetParameterWithNonDefaultParameterNotifyInvalidationListener() { + ParametersBuilder builder = defaultParameters.buildUpon().setPreferredAudioLanguage("eng"); + trackSelector.setParameters(builder); verify(invalidationListener).onTrackSelectionsInvalidated(); } /** - * Tests that track selector will not call - * {@link InvalidationListener#onTrackSelectionsInvalidated()} again when it's set with - * the same values of {@link Parameters}. + * Tests that track selector will not call {@link + * InvalidationListener#onTrackSelectionsInvalidated()} again when it's set with the same values + * of {@link Parameters}. */ @Test - public void testSetParameterWithSameParametersDoesNotNotifyInvalidationListenerAgain() - throws Exception { - ParametersBuilder builder = Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage("eng"); - trackSelector.setParameters(builder.build()); - trackSelector.setParameters(builder.build()); + public void testSetParameterWithSameParametersDoesNotNotifyInvalidationListenerAgain() { + ParametersBuilder builder = defaultParameters.buildUpon().setPreferredAudioLanguage("eng"); + trackSelector.setParameters(builder); + trackSelector.setParameters(builder); verify(invalidationListener, times(1)).onTrackSelectionsInvalidated(); } @@ -426,8 +428,7 @@ public final class DefaultTrackSelectorTest { Format.NO_VALUE, 2, 44100, null, null, 0, "eng"); TrackGroupArray trackGroups = wrapFormats(frAudioFormat, enAudioFormat); - trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage("eng").build()); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredAudioLanguage("eng")); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, @@ -452,8 +453,7 @@ public final class DefaultTrackSelectorTest { Format.NO_VALUE, 2, 44100, null, null, 0, "eng"); TrackGroupArray trackGroups = wrapFormats(frAudioFormat, enAudioFormat); - trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage("eng").build()); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredAudioLanguage("eng")); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, @@ -523,7 +523,7 @@ public final class DefaultTrackSelectorTest { TrackGroupArray trackGroups = singleTrackGroup(audioFormat); trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setExceedRendererCapabilitiesIfNecessary(false).build()); + defaultParameters.buildUpon().setExceedRendererCapabilitiesIfNecessary(false)); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES}, @@ -605,8 +605,7 @@ public final class DefaultTrackSelectorTest { RendererCapabilities mappedAudioRendererCapabilities = new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities); - trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage("eng").build()); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredAudioLanguage("eng")); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {mappedAudioRendererCapabilities}, @@ -648,8 +647,7 @@ public final class DefaultTrackSelectorTest { RendererCapabilities mappedAudioRendererCapabilities = new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities); - trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage("eng").build()); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredAudioLanguage("eng")); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {mappedAudioRendererCapabilities}, @@ -983,10 +981,7 @@ public final class DefaultTrackSelectorTest { // selected. trackGroups = wrapFormats(defaultOnly, noFlag, forcedOnly, forcedDefault); trackSelector.setParameters( - Parameters.DEFAULT - .buildUpon() - .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT) - .build()); + defaultParameters.buildUpon().setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT)); result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); assertNoSelection(result.selections.get(0)); @@ -997,15 +992,14 @@ public final class DefaultTrackSelectorTest { trackSelector .getParameters() .buildUpon() - .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT | C.SELECTION_FLAG_FORCED) - .build()); + .setDisabledTextTrackSelectionFlags( + C.SELECTION_FLAG_DEFAULT | C.SELECTION_FLAG_FORCED)); result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); assertNoSelection(result.selections.get(0)); // There is a preferred language, so a language-matching track flagged as default should // be selected, and the one without forced flag should be preferred. - trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setPreferredTextLanguage("eng").build()); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredTextLanguage("eng")); result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections.get(0), trackGroups, defaultOnly); @@ -1017,8 +1011,7 @@ public final class DefaultTrackSelectorTest { trackSelector .getParameters() .buildUpon() - .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT) - .build()); + .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT)); result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections.get(0), trackGroups, noFlag); } @@ -1100,12 +1093,12 @@ public final class DefaultTrackSelectorTest { assertNoSelection(result.selections.get(0)); trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setSelectUndeterminedTextLanguage(true).build()); + defaultParameters.buildUpon().setSelectUndeterminedTextLanguage(true)); result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections.get(0), trackGroups, undeterminedUnd); - ParametersBuilder builder = Parameters.DEFAULT.buildUpon().setPreferredTextLanguage("spa"); - trackSelector.setParameters(builder.build()); + ParametersBuilder builder = defaultParameters.buildUpon().setPreferredTextLanguage("spa"); + trackSelector.setParameters(builder); result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections.get(0), trackGroups, spanish); @@ -1114,7 +1107,7 @@ public final class DefaultTrackSelectorTest { result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE); assertNoSelection(result.selections.get(0)); - trackSelector.setParameters(builder.setSelectUndeterminedTextLanguage(true).build()); + trackSelector.setParameters(builder.setSelectUndeterminedTextLanguage(true)); result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections.get(0), trackGroups, undeterminedUnd); @@ -1158,13 +1151,13 @@ public final class DefaultTrackSelectorTest { assertNoSelection(result.selections.get(1)); // Explicit language preference for english. First renderer should be used. - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setPreferredTextLanguage("en")); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredTextLanguage("en")); result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections.get(0), trackGroups, english); assertNoSelection(result.selections.get(1)); // Explicit language preference for German. Second renderer should be used. - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setPreferredTextLanguage("de")); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredTextLanguage("de")); result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); assertNoSelection(result.selections.get(0)); assertFixedSelection(result.selections.get(1), trackGroups, german); @@ -1190,7 +1183,7 @@ public final class DefaultTrackSelectorTest { RendererCapabilities mappedAudioRendererCapabilities = new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities); - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setForceLowestBitrate(true).build()); + trackSelector.setParameters(defaultParameters.buildUpon().setForceLowestBitrate(true)); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {mappedAudioRendererCapabilities}, @@ -1221,7 +1214,7 @@ public final class DefaultTrackSelectorTest { new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities); trackSelector.setParameters( - new ParametersBuilder().setForceHighestSupportedBitrate(true).build()); + defaultParameters.buildUpon().setForceHighestSupportedBitrate(true)); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {mappedAudioRendererCapabilities}, @@ -1269,7 +1262,7 @@ public final class DefaultTrackSelectorTest { // If we explicitly enable mixed sample rate adaptiveness, expect an adaptive selection. trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setAllowAudioMixedSampleRateAdaptiveness(true)); + defaultParameters.buildUpon().setAllowAudioMixedSampleRateAdaptiveness(true)); result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1301,7 +1294,7 @@ public final class DefaultTrackSelectorTest { // If we explicitly enable mixed mime type adaptiveness, expect an adaptive selection. trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setAllowAudioMixedMimeTypeAdaptiveness(true)); + defaultParameters.buildUpon().setAllowAudioMixedMimeTypeAdaptiveness(true)); result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1335,7 +1328,7 @@ public final class DefaultTrackSelectorTest { // If we constrain the channel count to 4 we expect a fixed selection containing the track with // fewer channels. - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setMaxAudioChannelCount(4)); + trackSelector.setParameters(defaultParameters.buildUpon().setMaxAudioChannelCount(4)); result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1344,7 +1337,7 @@ public final class DefaultTrackSelectorTest { // If we constrain the channel count to 2 we expect a fixed selection containing the track with // fewer channels. - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setMaxAudioChannelCount(2)); + trackSelector.setParameters(defaultParameters.buildUpon().setMaxAudioChannelCount(2)); result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1353,7 +1346,7 @@ public final class DefaultTrackSelectorTest { // If we constrain the channel count to 1 we expect a fixed selection containing the track with // fewer channels. - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setMaxAudioChannelCount(1)); + trackSelector.setParameters(defaultParameters.buildUpon().setMaxAudioChannelCount(1)); result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1362,7 +1355,7 @@ public final class DefaultTrackSelectorTest { // If we disable exceeding of constraints we expect no selection. trackSelector.setParameters( - Parameters.DEFAULT + defaultParameters .buildUpon() .setMaxAudioChannelCount(1) .setExceedAudioConstraintsIfNecessary(false)); @@ -1424,13 +1417,13 @@ public final class DefaultTrackSelectorTest { assertNoSelection(result.selections.get(1)); // Explicit language preference for english. First renderer should be used. - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage("en")); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredAudioLanguage("en")); result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections.get(0), trackGroups, english); assertNoSelection(result.selections.get(1)); // Explicit language preference for German. Second renderer should be used. - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage("de")); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredAudioLanguage("de")); result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); assertNoSelection(result.selections.get(0)); assertFixedSelection(result.selections.get(1), trackGroups, german); @@ -1456,7 +1449,7 @@ public final class DefaultTrackSelectorTest { // Should do non-seamless adaptiveness by default, so expect an adaptive selection. TrackGroupArray trackGroups = singleTrackGroup(buildVideoFormat("0"), buildVideoFormat("1")); trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setAllowVideoNonSeamlessAdaptiveness(true)); + defaultParameters.buildUpon().setAllowVideoNonSeamlessAdaptiveness(true)); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {nonSeamlessVideoCapabilities}, @@ -1468,7 +1461,7 @@ public final class DefaultTrackSelectorTest { // If we explicitly disable non-seamless adaptiveness, expect a fixed selection. trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setAllowVideoNonSeamlessAdaptiveness(false)); + defaultParameters.buildUpon().setAllowVideoNonSeamlessAdaptiveness(false)); result = trackSelector.selectTracks( new RendererCapabilities[] {nonSeamlessVideoCapabilities}, @@ -1503,7 +1496,7 @@ public final class DefaultTrackSelectorTest { // If we explicitly enable mixed mime type adaptiveness, expect an adaptive selection. trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setAllowVideoMixedMimeTypeAdaptiveness(true)); + defaultParameters.buildUpon().setAllowVideoMixedMimeTypeAdaptiveness(true)); result = trackSelector.selectTracks( new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1760,13 +1753,13 @@ public final class DefaultTrackSelectorTest { } @Override - public int supportsFormat(Format format) throws ExoPlaybackException { + public int supportsFormat(Format format) { return MimeTypes.getTrackType(format.sampleMimeType) == trackType ? (supportValue) : FORMAT_UNSUPPORTED_TYPE; } @Override - public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { + public int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_SEAMLESS; } @@ -1801,14 +1794,14 @@ public final class DefaultTrackSelectorTest { } @Override - public int supportsFormat(Format format) throws ExoPlaybackException { + public int supportsFormat(Format format) { return format.id != null && formatToCapability.containsKey(format.id) ? formatToCapability.get(format.id) : FORMAT_UNSUPPORTED_TYPE; } @Override - public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { + public int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_SEAMLESS; } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java index 73225f68c7..107bf7c790 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.dash.offline; import android.net.Uri; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -31,6 +32,7 @@ public final class DownloadHelperTest { @Test public void staticDownloadHelperForDash_doesNotThrow() { DownloadHelper.forDash( + ApplicationProvider.getApplicationContext(), Uri.parse("http://uri"), new FakeDataSource.Factory(), (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0]); @@ -39,6 +41,6 @@ public final class DownloadHelperTest { new FakeDataSource.Factory(), (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0], /* drmSessionManager= */ DrmSessionManager.getDummyDrmSessionManager(), - DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS); + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); } } diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/DownloadHelperTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/DownloadHelperTest.java index c7a8034ee7..3c81074c25 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/DownloadHelperTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/DownloadHelperTest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.hls.offline; import android.net.Uri; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.offline.DownloadHelper; @@ -30,6 +31,7 @@ public final class DownloadHelperTest { @Test public void staticDownloadHelperForHls_doesNotThrow() { DownloadHelper.forHls( + ApplicationProvider.getApplicationContext(), Uri.parse("http://uri"), new FakeDataSource.Factory(), (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0]); @@ -38,6 +40,6 @@ public final class DownloadHelperTest { new FakeDataSource.Factory(), (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0], /* drmSessionManager= */ null, - DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS); + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); } } diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/DownloadHelperTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/DownloadHelperTest.java index 4da08f7631..a103f89cec 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/DownloadHelperTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/DownloadHelperTest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.smoothstreaming.offline; import android.net.Uri; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.offline.DownloadHelper; @@ -30,6 +31,7 @@ public final class DownloadHelperTest { @Test public void staticDownloadHelperForSmoothStreaming_doesNotThrow() { DownloadHelper.forSmoothStreaming( + ApplicationProvider.getApplicationContext(), Uri.parse("http://uri"), new FakeDataSource.Factory(), (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0]); @@ -38,6 +40,6 @@ public final class DownloadHelperTest { new FakeDataSource.Factory(), (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0], /* drmSessionManager= */ null, - DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS); + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); } } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index b2a49a31fe..e452e391d5 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -24,7 +24,6 @@ import android.net.Uri; import android.view.Surface; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; @@ -385,8 +384,7 @@ public final class DashTestRunner { MappedTrackInfo mappedTrackInfo, int[][][] rendererFormatSupports, int[] rendererMixedMimeTypeAdaptationSupports, - Parameters parameters) - throws ExoPlaybackException { + Parameters parameters) { Assertions.checkState( mappedTrackInfo.getRendererType(VIDEO_RENDERER_INDEX) == C.TRACK_TYPE_VIDEO); Assertions.checkState( diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index 90f2294bfc..3ebd47b7a6 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -37,7 +37,6 @@ import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.HostActivity.HostedTest; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.util.Clock; @@ -238,7 +237,7 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { } protected DefaultTrackSelector buildTrackSelector(HostActivity host) { - return new DefaultTrackSelector(new AdaptiveTrackSelection.Factory()); + return new DefaultTrackSelector(host); } protected SimpleExoPlayer buildExoPlayer( 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 b61c5f9b2c..9de7996d3c 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 @@ -284,7 +284,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc supportedFormats = new Format[] {VIDEO_FORMAT}; } if (trackSelector == null) { - trackSelector = new DefaultTrackSelector(); + trackSelector = new DefaultTrackSelector(context); } if (bandwidthMeter == null) { bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build(); From 3051e5e9adcb059d9692d5f05ffd2e6377eeda70 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 29 Jul 2019 16:08:37 +0100 Subject: [PATCH 1493/1556] Ensure the SilenceMediaSource position is in range Issue: #6229 PiperOrigin-RevId: 260500986 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/source/SilenceMediaSource.java | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7afe0a78cd..5279a24698 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -40,6 +40,8 @@ ([#6153](https://github.com/google/ExoPlayer/issues/6153)). * Fix `DataSchemeDataSource` re-opening and range requests ([#6192](https://github.com/google/ExoPlayer/issues/6192)). +* Ensure the `SilenceMediaSource` position is in range + ([#6229](https://github.com/google/ExoPlayer/issues/6229)). * Flac extension: Parse `VORBIS_COMMENT` metadata ([#5527](https://github.com/google/ExoPlayer/issues/5527)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java index a5b78ef3f7..c3eab68983 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java @@ -117,6 +117,7 @@ public final class SilenceMediaSource extends BaseMediaSource { @NullableType SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + positionUs = constrainSeekPosition(positionUs); for (int i = 0; i < selections.length; i++) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { sampleStreams.remove(streams[i]); @@ -143,6 +144,7 @@ public final class SilenceMediaSource extends BaseMediaSource { @Override public long seekToUs(long positionUs) { + positionUs = constrainSeekPosition(positionUs); for (int i = 0; i < sampleStreams.size(); i++) { ((SilenceSampleStream) sampleStreams.get(i)).seekTo(positionUs); } @@ -151,7 +153,7 @@ public final class SilenceMediaSource extends BaseMediaSource { @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - return positionUs; + return constrainSeekPosition(positionUs); } @Override @@ -171,6 +173,10 @@ public final class SilenceMediaSource extends BaseMediaSource { @Override public void reevaluateBuffer(long positionUs) {} + + private long constrainSeekPosition(long positionUs) { + return Util.constrainValue(positionUs, 0, durationUs); + } } private static final class SilenceSampleStream implements SampleStream { @@ -186,7 +192,7 @@ public final class SilenceMediaSource extends BaseMediaSource { } public void seekTo(long positionUs) { - positionBytes = getAudioByteCount(positionUs); + positionBytes = Util.constrainValue(getAudioByteCount(positionUs), 0, durationBytes); } @Override From 06f94815050b9f9e990c7c9d3d7b7eb0bbebecc1 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 29 Jul 2019 17:41:04 +0100 Subject: [PATCH 1494/1556] Support different drm schemes in playlists in the demo app This CL changes PlayerActivity's VIEW_LIST action intent contract: Each media item configuration is provided by indexing the entries. For example, the URI of the first item is passed as "uri_0", the second one is "uri_1", etc. Optionally, the extra parameters, like the extensions, are passed as "extension_1", where the intent extras with matching indices, refer to the same media sample. The VIEW action's contract remains unchanged. PiperOrigin-RevId: 260518118 --- .../exoplayer2/demo/PlayerActivity.java | 275 ++++++++++-------- .../android/exoplayer2/demo/Sample.java | 187 ++++++++++++ .../demo/SampleChooserActivity.java | 148 ++-------- demos/main/src/main/res/values/strings.xml | 2 + 4 files changed, 360 insertions(+), 252 deletions(-) create mode 100644 demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java 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 d8bfe23674..1e231dd45e 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 @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.demo.Sample.UriSample; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; @@ -78,41 +79,48 @@ import java.lang.reflect.Constructor; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; +import java.util.ArrayList; import java.util.UUID; /** An activity that plays media using {@link SimpleExoPlayer}. */ public class PlayerActivity extends AppCompatActivity implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener { - public static final String DRM_SCHEME_EXTRA = "drm_scheme"; - public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; - public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties"; - public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session"; - public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; - - public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; - public static final String EXTENSION_EXTRA = "extension"; - - public static final String ACTION_VIEW_LIST = - "com.google.android.exoplayer.demo.action.VIEW_LIST"; - public static final String URI_LIST_EXTRA = "uri_list"; - public static final String EXTENSION_LIST_EXTRA = "extension_list"; - - public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; - - public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm"; - public static final String ABR_ALGORITHM_DEFAULT = "default"; - public static final String ABR_ALGORITHM_RANDOM = "random"; + // Activity extras. public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode"; public static final String SPHERICAL_STEREO_MODE_MONO = "mono"; public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom"; public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right"; + // Actions. + + public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; + public static final String ACTION_VIEW_LIST = + "com.google.android.exoplayer.demo.action.VIEW_LIST"; + + // Player configuration extras. + + public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm"; + public static final String ABR_ALGORITHM_DEFAULT = "default"; + public static final String ABR_ALGORITHM_RANDOM = "random"; + + // Media item configuration extras. + + public static final String URI_EXTRA = "uri"; + public static final String EXTENSION_EXTRA = "extension"; + + public static final String DRM_SCHEME_EXTRA = "drm_scheme"; + public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; + public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties"; + public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session"; + public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; + public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; // For backwards compatibility only. - private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; + public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; // Saved instance state keys. + private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters"; private static final String KEY_WINDOW = "window"; private static final String KEY_POSITION = "position"; @@ -124,6 +132,8 @@ public class PlayerActivity extends AppCompatActivity DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); } + private final ArrayList mediaDrms; + private PlayerView playerView; private LinearLayout debugRootView; private Button selectTracksButton; @@ -132,7 +142,6 @@ public class PlayerActivity extends AppCompatActivity private DataSource.Factory dataSourceFactory; private SimpleExoPlayer player; - private FrameworkMediaDrm mediaDrm; private MediaSource mediaSource; private DefaultTrackSelector trackSelector; private DefaultTrackSelector.Parameters trackSelectorParameters; @@ -148,6 +157,10 @@ public class PlayerActivity extends AppCompatActivity private AdsLoader adsLoader; private Uri loadedAdTagUri; + public PlayerActivity() { + mediaDrms = new ArrayList<>(); + } + // Activity lifecycle @Override @@ -329,69 +342,11 @@ public class PlayerActivity extends AppCompatActivity private void initializePlayer() { if (player == null) { Intent intent = getIntent(); - String action = intent.getAction(); - Uri[] uris; - String[] extensions; - if (ACTION_VIEW.equals(action)) { - uris = new Uri[] {intent.getData()}; - extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)}; - } else if (ACTION_VIEW_LIST.equals(action)) { - String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); - uris = new Uri[uriStrings.length]; - for (int i = 0; i < uriStrings.length; i++) { - uris[i] = Uri.parse(uriStrings[i]); - } - extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA); - if (extensions == null) { - extensions = new String[uriStrings.length]; - } - } else { - showToast(getString(R.string.unexpected_intent_action, action)); - finish(); - return; - } - if (!Util.checkCleartextTrafficPermitted(uris)) { - showToast(R.string.error_cleartext_not_permitted); - return; - } - if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, uris)) { - // The player will be reinitialized if the permission is granted. - return; - } - DrmSessionManager drmSessionManager = null; - if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) { - String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA); - String[] keyRequestPropertiesArray = - intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA); - boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA, false); - int errorStringId = R.string.error_drm_unknown; - if (Util.SDK_INT < 18) { - errorStringId = R.string.error_drm_not_supported; - } else { - try { - String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA - : DRM_SCHEME_UUID_EXTRA; - UUID drmSchemeUuid = Util.getDrmUuid(intent.getStringExtra(drmSchemeExtra)); - if (drmSchemeUuid == null) { - errorStringId = R.string.error_drm_unsupported_scheme; - } else { - drmSessionManager = - buildDrmSessionManagerV18( - drmSchemeUuid, drmLicenseUrl, keyRequestPropertiesArray, multiSession); - } - } catch (UnsupportedDrmException e) { - errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME - ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown; - } - } - if (drmSessionManager == null) { - showToast(errorStringId); - finish(); - return; - } - } else { - drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); + releaseMediaDrms(); + mediaSource = createTopLevelMediaSource(intent); + if (mediaSource == null) { + return; } TrackSelection.Factory trackSelectionFactory; @@ -424,28 +379,8 @@ public class PlayerActivity extends AppCompatActivity playerView.setPlaybackPreparer(this); debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); - - MediaSource[] mediaSources = new MediaSource[uris.length]; - for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i], drmSessionManager); - } - mediaSource = - mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); - String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA); - if (adTagUriString != null) { - Uri adTagUri = Uri.parse(adTagUriString); - if (!adTagUri.equals(loadedAdTagUri)) { - releaseAdsLoader(); - loadedAdTagUri = adTagUri; - } - MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString)); - if (adsMediaSource != null) { - mediaSource = adsMediaSource; - } else { - showToast(R.string.ima_not_loaded); - } - } else { - releaseAdsLoader(); + if (adsLoader != null) { + adsLoader.setPlayer(player); } } boolean haveStartPosition = startWindow != C.INDEX_UNSET; @@ -456,23 +391,113 @@ public class PlayerActivity extends AppCompatActivity updateButtonVisibility(); } - private MediaSource buildMediaSource(Uri uri) { - return buildMediaSource( - uri, - /* overrideExtension= */ null, - /* drmSessionManager= */ DrmSessionManager.getDummyDrmSessionManager()); + @Nullable + private MediaSource createTopLevelMediaSource(Intent intent) { + String action = intent.getAction(); + boolean actionIsListView = ACTION_VIEW_LIST.equals(action); + if (!actionIsListView && !ACTION_VIEW.equals(action)) { + showToast(getString(R.string.unexpected_intent_action, action)); + finish(); + return null; + } + + Sample intentAsSample = Sample.createFromIntent(intent); + UriSample[] samples = + intentAsSample instanceof Sample.PlaylistSample + ? ((Sample.PlaylistSample) intentAsSample).children + : new UriSample[] {(UriSample) intentAsSample}; + + boolean seenAdsTagUri = false; + for (UriSample sample : samples) { + seenAdsTagUri |= sample.adTagUri != null; + if (!Util.checkCleartextTrafficPermitted(sample.uri)) { + showToast(R.string.error_cleartext_not_permitted); + return null; + } + if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri)) { + // The player will be reinitialized if the permission is granted. + return null; + } + } + + MediaSource[] mediaSources = new MediaSource[samples.length]; + for (int i = 0; i < samples.length; i++) { + mediaSources[i] = createLeafMediaSource(samples[i]); + } + MediaSource mediaSource = + mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); + + if (seenAdsTagUri) { + Uri adTagUri = samples[0].adTagUri; + if (actionIsListView) { + showToast(R.string.unsupported_ads_in_concatenation); + } else { + if (!adTagUri.equals(loadedAdTagUri)) { + releaseAdsLoader(); + loadedAdTagUri = adTagUri; + } + MediaSource adsMediaSource = createAdsMediaSource(mediaSource, adTagUri); + if (adsMediaSource != null) { + mediaSource = adsMediaSource; + } else { + showToast(R.string.ima_not_loaded); + } + } + } else { + releaseAdsLoader(); + } + + return mediaSource; } - private MediaSource buildMediaSource( - Uri uri, - @Nullable String overrideExtension, - DrmSessionManager drmSessionManager) { + private MediaSource createLeafMediaSource(UriSample parameters) { + DrmSessionManager drmSessionManager = null; + Sample.DrmInfo drmInfo = parameters.drmInfo; + if (drmInfo != null) { + int errorStringId = R.string.error_drm_unknown; + if (Util.SDK_INT < 18) { + errorStringId = R.string.error_drm_not_supported; + } else { + try { + if (drmInfo.drmScheme == null) { + errorStringId = R.string.error_drm_unsupported_scheme; + } else { + drmSessionManager = + buildDrmSessionManagerV18( + drmInfo.drmScheme, + drmInfo.drmLicenseUrl, + drmInfo.drmKeyRequestProperties, + drmInfo.drmMultiSession); + } + } catch (UnsupportedDrmException e) { + errorStringId = + e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME + ? R.string.error_drm_unsupported_scheme + : R.string.error_drm_unknown; + } + } + if (drmSessionManager == null) { + showToast(errorStringId); + finish(); + return null; + } + } else { + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); + } + DownloadRequest downloadRequest = - ((DemoApplication) getApplication()).getDownloadTracker().getDownloadRequest(uri); + ((DemoApplication) getApplication()) + .getDownloadTracker() + .getDownloadRequest(parameters.uri); if (downloadRequest != null) { return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory); } - @ContentType int type = Util.inferContentType(uri, overrideExtension); + return createLeafMediaSource(parameters.uri, parameters.extension, drmSessionManager); + } + + private MediaSource createLeafMediaSource( + Uri uri, String extension, DrmSessionManager drmSessionManager) { + @ContentType int type = Util.inferContentType(uri, extension); switch (type) { case C.TYPE_DASH: return new DashMediaSource.Factory(dataSourceFactory) @@ -508,8 +533,9 @@ public class PlayerActivity extends AppCompatActivity keyRequestPropertiesArray[i + 1]); } } - releaseMediaDrm(); - mediaDrm = FrameworkMediaDrm.newInstance(uuid); + + FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid); + mediaDrms.add(mediaDrm); return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession); } @@ -527,14 +553,14 @@ public class PlayerActivity extends AppCompatActivity if (adsLoader != null) { adsLoader.setPlayer(null); } - releaseMediaDrm(); + releaseMediaDrms(); } - private void releaseMediaDrm() { - if (mediaDrm != null) { + private void releaseMediaDrms() { + for (FrameworkMediaDrm mediaDrm : mediaDrms) { mediaDrm.release(); - mediaDrm = null; } + mediaDrms.clear(); } private void releaseAdsLoader() { @@ -588,12 +614,12 @@ public class PlayerActivity extends AppCompatActivity // LINT.ThenChange(../../../../../../../../proguard-rules.txt) adsLoader = loaderConstructor.newInstance(this, adTagUri); } - adsLoader.setPlayer(player); MediaSourceFactory adMediaSourceFactory = new MediaSourceFactory() { @Override public MediaSource createMediaSource(Uri uri) { - return PlayerActivity.this.buildMediaSource(uri); + return PlayerActivity.this.createLeafMediaSource( + uri, /* extension=*/ null, DrmSessionManager.getDummyDrmSessionManager()); } @Override @@ -718,5 +744,4 @@ public class PlayerActivity extends AppCompatActivity return Pair.create(0, errorString); } } - } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java new file mode 100644 index 0000000000..4497b9a984 --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2019 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.demo; + +import static com.google.android.exoplayer2.demo.PlayerActivity.ACTION_VIEW_LIST; +import static com.google.android.exoplayer2.demo.PlayerActivity.AD_TAG_URI_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_LICENSE_URL_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_MULTI_SESSION_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_UUID_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.URI_EXTRA; + +import android.content.Intent; +import android.net.Uri; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.UUID; + +/* package */ abstract class Sample { + + public static final class UriSample extends Sample { + + public static UriSample createFromIntent(Uri uri, Intent intent, String extrasKeySuffix) { + String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix); + String adsTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix); + Uri adTagUri = adsTagUriString != null ? Uri.parse(adsTagUriString) : null; + return new UriSample( + /* name= */ null, + DrmInfo.createFromIntent(intent, extrasKeySuffix), + uri, + extension, + adTagUri, + /* sphericalStereoMode= */ null); + } + + public final Uri uri; + public final String extension; + public final DrmInfo drmInfo; + public final Uri adTagUri; + public final String sphericalStereoMode; + + public UriSample( + String name, + DrmInfo drmInfo, + Uri uri, + String extension, + Uri adTagUri, + String sphericalStereoMode) { + super(name); + this.uri = uri; + this.extension = extension; + this.drmInfo = drmInfo; + this.adTagUri = adTagUri; + this.sphericalStereoMode = sphericalStereoMode; + } + + @Override + public void addToIntent(Intent intent) { + intent.setAction(PlayerActivity.ACTION_VIEW).setData(uri); + intent.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode); + addPlayerConfigToIntent(intent, /* extrasKeySuffix= */ ""); + } + + public void addToPlaylistIntent(Intent intent, String extrasKeySuffix) { + intent.putExtra(PlayerActivity.URI_EXTRA + extrasKeySuffix, uri.toString()); + addPlayerConfigToIntent(intent, extrasKeySuffix); + } + + private void addPlayerConfigToIntent(Intent intent, String extrasKeySuffix) { + intent + .putExtra(EXTENSION_EXTRA + extrasKeySuffix, extension) + .putExtra( + AD_TAG_URI_EXTRA + extrasKeySuffix, adTagUri != null ? adTagUri.toString() : null); + if (drmInfo != null) { + drmInfo.addToIntent(intent, extrasKeySuffix); + } + } + } + + public static final class PlaylistSample extends Sample { + + public final UriSample[] children; + + public PlaylistSample(String name, UriSample... children) { + super(name); + this.children = children; + } + + @Override + public void addToIntent(Intent intent) { + intent.setAction(PlayerActivity.ACTION_VIEW_LIST); + for (int i = 0; i < children.length; i++) { + children[i].addToPlaylistIntent(intent, /* extrasKeySuffix= */ "_" + i); + } + } + } + + public static final class DrmInfo { + + public static DrmInfo createFromIntent(Intent intent, String extrasKeySuffix) { + String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix; + String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix; + if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) { + return null; + } + String drmSchemeExtra = + intent.hasExtra(schemeKey) + ? intent.getStringExtra(schemeKey) + : intent.getStringExtra(schemeUuidKey); + UUID drmScheme = Util.getDrmUuid(drmSchemeExtra); + String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix); + String[] keyRequestPropertiesArray = + intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix); + boolean drmMultiSession = + intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false); + return new DrmInfo(drmScheme, drmLicenseUrl, keyRequestPropertiesArray, drmMultiSession); + } + + public final UUID drmScheme; + public final String drmLicenseUrl; + public final String[] drmKeyRequestProperties; + public final boolean drmMultiSession; + + public DrmInfo( + UUID drmScheme, + String drmLicenseUrl, + String[] drmKeyRequestProperties, + boolean drmMultiSession) { + this.drmScheme = drmScheme; + this.drmLicenseUrl = drmLicenseUrl; + this.drmKeyRequestProperties = drmKeyRequestProperties; + this.drmMultiSession = drmMultiSession; + } + + public void addToIntent(Intent intent, String extrasKeySuffix) { + Assertions.checkNotNull(intent); + intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmScheme.toString()); + intent.putExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix, drmLicenseUrl); + intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties); + intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmMultiSession); + } + } + + public static Sample createFromIntent(Intent intent) { + if (ACTION_VIEW_LIST.equals(intent.getAction())) { + ArrayList intentUris = new ArrayList<>(); + int index = 0; + while (intent.hasExtra(URI_EXTRA + "_" + index)) { + intentUris.add(intent.getStringExtra(URI_EXTRA + "_" + index)); + index++; + } + UriSample[] children = new UriSample[intentUris.size()]; + for (int i = 0; i < children.length; i++) { + Uri uri = Uri.parse(intentUris.get(i)); + children[i] = UriSample.createFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + i); + } + return new PlaylistSample(/* name= */ null, children); + } else { + return UriSample.createFromIntent(intent.getData(), intent, /* extrasKeySuffix= */ ""); + } + } + + @Nullable public final String name; + + public Sample(String name) { + this.name = name; + } + + public abstract void addToIntent(Intent intent); +} 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 7245de01c6..09fa62e51a 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 @@ -38,6 +38,9 @@ import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.demo.Sample.DrmInfo; +import com.google.android.exoplayer2.demo.Sample.PlaylistSample; +import com.google.android.exoplayer2.demo.Sample.UriSample; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceInputStream; @@ -161,13 +164,17 @@ public class SampleChooserActivity extends AppCompatActivity public boolean onChildClick( ExpandableListView parent, View view, int groupPosition, int childPosition, long id) { Sample sample = (Sample) view.getTag(); - startActivity( - sample.buildIntent( - /* context= */ this, - isNonNullAndChecked(preferExtensionDecodersMenuItem), - isNonNullAndChecked(randomAbrMenuItem) - ? PlayerActivity.ABR_ALGORITHM_RANDOM - : PlayerActivity.ABR_ALGORITHM_DEFAULT)); + Intent intent = new Intent(this, PlayerActivity.class); + intent.putExtra( + PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, + isNonNullAndChecked(preferExtensionDecodersMenuItem)); + String abrAlgorithm = + isNonNullAndChecked(randomAbrMenuItem) + ? PlayerActivity.ABR_ALGORITHM_RANDOM + : PlayerActivity.ABR_ALGORITHM_DEFAULT; + intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm); + sample.addToIntent(intent); + startActivity(intent); return true; } @@ -309,17 +316,12 @@ public class SampleChooserActivity extends AppCompatActivity extension = reader.nextString(); break; case "drm_scheme": - Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme"); drmScheme = reader.nextString(); break; case "drm_license_url": - Assertions.checkState(!insidePlaylist, - "Invalid attribute on nested item: drm_license_url"); drmLicenseUrl = reader.nextString(); break; case "drm_key_request_properties": - Assertions.checkState(!insidePlaylist, - "Invalid attribute on nested item: drm_key_request_properties"); ArrayList drmKeyRequestPropertiesList = new ArrayList<>(); reader.beginObject(); while (reader.hasNext()) { @@ -357,17 +359,21 @@ public class SampleChooserActivity extends AppCompatActivity DrmInfo drmInfo = drmScheme == null ? null - : new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession); + : new DrmInfo( + Util.getDrmUuid(drmScheme), + drmLicenseUrl, + drmKeyRequestProperties, + drmMultiSession); if (playlistSamples != null) { UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]); - return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray); + return new PlaylistSample(sampleName, playlistSamplesArray); } else { return new UriSample( sampleName, drmInfo, uri, extension, - adTagUri, + adTagUri != null ? Uri.parse(adTagUri) : null, sphericalStereoMode); } } @@ -497,116 +503,4 @@ public class SampleChooserActivity extends AppCompatActivity } } - - private static final class DrmInfo { - public final String drmScheme; - public final String drmLicenseUrl; - public final String[] drmKeyRequestProperties; - public final boolean drmMultiSession; - - public DrmInfo( - String drmScheme, - String drmLicenseUrl, - String[] drmKeyRequestProperties, - boolean drmMultiSession) { - this.drmScheme = drmScheme; - this.drmLicenseUrl = drmLicenseUrl; - this.drmKeyRequestProperties = drmKeyRequestProperties; - this.drmMultiSession = drmMultiSession; - } - - public void updateIntent(Intent intent) { - Assertions.checkNotNull(intent); - intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmScheme); - intent.putExtra(PlayerActivity.DRM_LICENSE_URL_EXTRA, drmLicenseUrl); - intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA, drmKeyRequestProperties); - intent.putExtra(PlayerActivity.DRM_MULTI_SESSION_EXTRA, drmMultiSession); - } - } - - private abstract static class Sample { - public final String name; - public final DrmInfo drmInfo; - - public Sample(String name, DrmInfo drmInfo) { - this.name = name; - this.drmInfo = drmInfo; - } - - public Intent buildIntent( - Context context, boolean preferExtensionDecoders, String abrAlgorithm) { - Intent intent = new Intent(context, PlayerActivity.class); - intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders); - intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm); - if (drmInfo != null) { - drmInfo.updateIntent(intent); - } - return intent; - } - - } - - private static final class UriSample extends Sample { - - public final Uri uri; - public final String extension; - public final String adTagUri; - public final String sphericalStereoMode; - - public UriSample( - String name, - DrmInfo drmInfo, - Uri uri, - String extension, - String adTagUri, - String sphericalStereoMode) { - super(name, drmInfo); - this.uri = uri; - this.extension = extension; - this.adTagUri = adTagUri; - this.sphericalStereoMode = sphericalStereoMode; - } - - @Override - public Intent buildIntent( - Context context, boolean preferExtensionDecoders, String abrAlgorithm) { - return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm) - .setData(uri) - .putExtra(PlayerActivity.EXTENSION_EXTRA, extension) - .putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri) - .putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode) - .setAction(PlayerActivity.ACTION_VIEW); - } - - } - - private static final class PlaylistSample extends Sample { - - public final UriSample[] children; - - public PlaylistSample( - String name, - DrmInfo drmInfo, - UriSample... children) { - super(name, drmInfo); - this.children = children; - } - - @Override - public Intent buildIntent( - Context context, boolean preferExtensionDecoders, String abrAlgorithm) { - 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.toString(); - extensions[i] = children[i].extension; - } - return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm) - .putExtra(PlayerActivity.URI_LIST_EXTRA, uris) - .putExtra(PlayerActivity.EXTENSION_LIST_EXTRA, extensions) - .setAction(PlayerActivity.ACTION_VIEW_LIST); - } - - } - } diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml index 0729da2fc6..f74ce8c076 100644 --- a/demos/main/src/main/res/values/strings.xml +++ b/demos/main/src/main/res/values/strings.xml @@ -53,6 +53,8 @@ Playing sample without ads, as the IMA extension was not loaded + Playing sample without ads, as ads are not supported in concatenations + Failed to start download This demo app does not support downloading playlists From 961adb7e36f409c6a51e0d7f6ace017e3a23cd49 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 29 Jul 2019 19:54:39 +0100 Subject: [PATCH 1495/1556] Cast: Add MediaItemConverter For now this just moves some code from the demo app to the extension. Eventually the goal would be to have CastPlayer playlist methods take MediaItem, have CastPlayer convert them internally to MediaQueueItem for sending to the Cast SDK, and also allow reverse conversion so we can reconstruct MediaItems from the Cast SDK's queue. PiperOrigin-RevId: 260548020 --- .../exoplayer2/castdemo/PlayerManager.java | 59 ++------------ .../ext/cast/DefaultMediaItemConverter.java | 81 +++++++++++++++++++ .../ext/cast/MediaItemConverter.java | 32 ++++++++ 3 files changed, 119 insertions(+), 53 deletions(-) create mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java create mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java 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 421269772c..44d9a60ff2 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,7 +17,6 @@ package com.google.android.exoplayer2.castdemo; import android.content.Context; import android.net.Uri; -import androidx.annotation.Nullable; import android.view.KeyEvent; import android.view.View; import com.google.android.exoplayer2.C; @@ -30,7 +29,9 @@ import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.ext.cast.CastPlayer; +import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter; import com.google.android.exoplayer2.ext.cast.MediaItem; +import com.google.android.exoplayer2.ext.cast.MediaItemConverter; import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; @@ -41,13 +42,9 @@ import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.gms.cast.MediaInfo; -import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; import java.util.ArrayList; -import org.json.JSONException; -import org.json.JSONObject; /** Manages players and an internal media queue for the demo app. */ /* package */ class PlayerManager implements EventListener, SessionAvailabilityListener { @@ -70,6 +67,7 @@ import org.json.JSONObject; private final ArrayList mediaQueue; private final Listener listener; private final ConcatenatingMediaSource concatenatingMediaSource; + private final MediaItemConverter mediaItemConverter; private int currentItemIndex; private Player currentPlayer; @@ -95,6 +93,7 @@ import org.json.JSONObject; mediaQueue = new ArrayList<>(); currentItemIndex = C.INDEX_UNSET; concatenatingMediaSource = new ConcatenatingMediaSource(); + mediaItemConverter = new DefaultMediaItemConverter(); exoPlayer = ExoPlayerFactory.newSimpleInstance(context); exoPlayer.addListener(this); @@ -133,7 +132,7 @@ import org.json.JSONObject; mediaQueue.add(item); concatenatingMediaSource.addMediaSource(buildMediaSource(item)); if (currentPlayer == castPlayer) { - castPlayer.addItems(buildMediaQueueItem(item)); + castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item)); } } @@ -344,7 +343,7 @@ import org.json.JSONObject; if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) { MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; for (int i = 0; i < items.length; i++) { - items[i] = buildMediaQueueItem(mediaQueue.get(i)); + items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i)); } castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); } else { @@ -380,50 +379,4 @@ import org.json.JSONObject; throw new IllegalArgumentException("mimeType is unsupported: " + mimeType); } } - - private static MediaQueueItem buildMediaQueueItem(MediaItem item) { - MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); - movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title); - MediaInfo.Builder mediaInfoBuilder = - new MediaInfo.Builder(item.uri.toString()) - .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setContentType(item.mimeType) - .setMetadata(movieMetadata); - MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; - if (drmConfiguration != null) { - try { - // This configuration is only intended for testing and should *not* be used in production - // environments. See comment in the Cast Demo app's options provider. - JSONObject drmConfigurationJson = getDrmConfigurationJson(drmConfiguration); - if (drmConfigurationJson != null) { - mediaInfoBuilder.setCustomData(drmConfigurationJson); - } - } catch (JSONException e) { - throw new RuntimeException(e); - } - } - return new MediaQueueItem.Builder(mediaInfoBuilder.build()).build(); - } - - @Nullable - private static JSONObject getDrmConfigurationJson(MediaItem.DrmConfiguration drmConfiguration) - throws JSONException { - String drmScheme; - if (C.WIDEVINE_UUID.equals(drmConfiguration.uuid)) { - drmScheme = "widevine"; - } else if (C.PLAYREADY_UUID.equals(drmConfiguration.uuid)) { - drmScheme = "playready"; - } else { - return null; - } - JSONObject exoplayerConfig = - new JSONObject().put("withCredentials", false).put("protectionSystem", drmScheme); - if (drmConfiguration.licenseUri != null) { - exoplayerConfig.put("licenseUrl", drmConfiguration.licenseUri); - } - if (!drmConfiguration.requestHeaders.isEmpty()) { - exoplayerConfig.put("headers", new JSONObject(drmConfiguration.requestHeaders)); - } - return new JSONObject().put("exoPlayerConfig", exoplayerConfig); - } } diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java new file mode 100644 index 0000000000..c8db958d03 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2019 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.ext.cast; + +import com.google.android.exoplayer2.C; +import com.google.android.gms.cast.MediaInfo; +import com.google.android.gms.cast.MediaMetadata; +import com.google.android.gms.cast.MediaQueueItem; +import org.json.JSONException; +import org.json.JSONObject; + +/** Default {@link MediaItemConverter} implementation. */ +public final class DefaultMediaItemConverter implements MediaItemConverter { + + @Override + public MediaQueueItem toMediaQueueItem(MediaItem item) { + if (item.mimeType == null) { + throw new IllegalArgumentException("The item must specify its mimeType"); + } + MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); + if (item.title != null) { + movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title); + } + MediaInfo mediaInfo = + new MediaInfo.Builder(item.uri.toString()) + .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) + .setContentType(item.mimeType) + .setMetadata(movieMetadata) + .setCustomData(getCustomData(item)) + .build(); + return new MediaQueueItem.Builder(mediaInfo).build(); + } + + private static JSONObject getCustomData(MediaItem item) { + JSONObject customData = new JSONObject(); + + MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; + if (drmConfiguration == null) { + return customData; + } + + String drmScheme; + if (C.WIDEVINE_UUID.equals(drmConfiguration.uuid)) { + drmScheme = "widevine"; + } else if (C.PLAYREADY_UUID.equals(drmConfiguration.uuid)) { + drmScheme = "playready"; + } else { + return customData; + } + + JSONObject exoPlayerConfig = new JSONObject(); + try { + exoPlayerConfig.put("withCredentials", false); + exoPlayerConfig.put("protectionSystem", drmScheme); + if (drmConfiguration.licenseUri != null) { + exoPlayerConfig.put("licenseUrl", drmConfiguration.licenseUri); + } + if (!drmConfiguration.requestHeaders.isEmpty()) { + exoPlayerConfig.put("headers", new JSONObject(drmConfiguration.requestHeaders)); + } + customData.put("exoPlayerConfig", exoPlayerConfig); + } catch (JSONException e) { + throw new RuntimeException(e); + } + + return customData; + } +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java new file mode 100644 index 0000000000..3cb2540ad8 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 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.ext.cast; + +import com.google.android.gms.cast.MediaQueueItem; + +/** Converts between {@link MediaItem} and the Cast SDK's {@link MediaQueueItem}. */ +public interface MediaItemConverter { + + /** + * Converts a {@link MediaItem} to a {@link MediaQueueItem}. + * + * @param mediaItem The {@link MediaItem}. + * @return An equivalent {@link MediaQueueItem}. + */ + MediaQueueItem toMediaQueueItem(MediaItem mediaItem); + + // TODO: Add toMediaItem to convert in the opposite direction. +} From 46855884f583f95680e5928a0613ce494d08be58 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 29 Jul 2019 20:22:00 +0100 Subject: [PATCH 1496/1556] Fix samples' text in Cast demo app PiperOrigin-RevId: 260553467 --- .../android/exoplayer2/castdemo/MainActivity.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 1a7f28cd77..244025f90d 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer2.castdemo; import android.content.Context; import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.graphics.ColorUtils; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; @@ -281,5 +283,13 @@ public class MainActivity extends AppCompatActivity public SampleListAdapter(Context context) { super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = super.getView(position, convertView, parent); + ((TextView) view).setText(getItem(position).title); + return view; + } } } From 8be78d47ac5eeac8ae7b21abbe6e61c8f887f10e Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 29 Jul 2019 20:26:53 +0100 Subject: [PATCH 1497/1556] Cast: Add JSON serialization/deserialization for MediaItem This will allow the Cast extension to reconstruct MediaItems from MediaQueueItems obtained from the receiver's queue. PiperOrigin-RevId: 260554381 --- .../ext/cast/DefaultMediaItemConverter.java | 130 +++++++++++++++--- .../ext/cast/MediaItemConverter.java | 8 +- .../cast/DefaultMediaItemConverterTest.java | 66 +++++++++ 3 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java index c8db958d03..098803a512 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java @@ -15,41 +15,132 @@ */ package com.google.android.exoplayer2.ext.cast; +import android.net.Uri; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaQueueItem; +import java.util.HashMap; +import java.util.Iterator; +import java.util.UUID; import org.json.JSONException; import org.json.JSONObject; /** Default {@link MediaItemConverter} implementation. */ public final class DefaultMediaItemConverter implements MediaItemConverter { + private static final String KEY_MEDIA_ITEM = "mediaItem"; + private static final String KEY_PLAYER_CONFIG = "exoPlayerConfig"; + private static final String KEY_URI = "uri"; + private static final String KEY_TITLE = "title"; + private static final String KEY_MIME_TYPE = "mimeType"; + private static final String KEY_DRM_CONFIGURATION = "drmConfiguration"; + private static final String KEY_UUID = "uuid"; + private static final String KEY_LICENSE_URI = "licenseUri"; + private static final String KEY_REQUEST_HEADERS = "requestHeaders"; + + @Override + public MediaItem toMediaItem(MediaQueueItem item) { + return getMediaItem(item.getMedia().getCustomData()); + } + @Override public MediaQueueItem toMediaQueueItem(MediaItem item) { if (item.mimeType == null) { throw new IllegalArgumentException("The item must specify its mimeType"); } - MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); + MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); if (item.title != null) { - movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title); + metadata.putString(MediaMetadata.KEY_TITLE, item.title); } MediaInfo mediaInfo = new MediaInfo.Builder(item.uri.toString()) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setContentType(item.mimeType) - .setMetadata(movieMetadata) + .setMetadata(metadata) .setCustomData(getCustomData(item)) .build(); return new MediaQueueItem.Builder(mediaInfo).build(); } - private static JSONObject getCustomData(MediaItem item) { - JSONObject customData = new JSONObject(); + // Deserialization. - MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; + private static MediaItem getMediaItem(JSONObject customData) { + try { + JSONObject mediaItemJson = customData.getJSONObject(KEY_MEDIA_ITEM); + MediaItem.Builder builder = new MediaItem.Builder(); + builder.setUri(Uri.parse(mediaItemJson.getString(KEY_URI))); + if (mediaItemJson.has(KEY_TITLE)) { + builder.setTitle(mediaItemJson.getString(KEY_TITLE)); + } + if (mediaItemJson.has(KEY_MIME_TYPE)) { + builder.setMimeType(mediaItemJson.getString(KEY_MIME_TYPE)); + } + if (mediaItemJson.has(KEY_DRM_CONFIGURATION)) { + builder.setDrmConfiguration( + getDrmConfiguration(mediaItemJson.getJSONObject(KEY_DRM_CONFIGURATION))); + } + return builder.build(); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + private static DrmConfiguration getDrmConfiguration(JSONObject json) throws JSONException { + UUID uuid = UUID.fromString(json.getString(KEY_UUID)); + Uri licenseUri = Uri.parse(json.getString(KEY_LICENSE_URI)); + JSONObject requestHeadersJson = json.getJSONObject(KEY_REQUEST_HEADERS); + HashMap requestHeaders = new HashMap<>(); + for (Iterator iterator = requestHeadersJson.keys(); iterator.hasNext(); ) { + String key = iterator.next(); + requestHeaders.put(key, requestHeadersJson.getString(key)); + } + return new DrmConfiguration(uuid, licenseUri, requestHeaders); + } + + // Serialization. + + private static JSONObject getCustomData(MediaItem item) { + JSONObject json = new JSONObject(); + try { + json.put(KEY_MEDIA_ITEM, getMediaItemJson(item)); + JSONObject playerConfigJson = getPlayerConfigJson(item); + if (playerConfigJson != null) { + json.put(KEY_PLAYER_CONFIG, playerConfigJson); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + return json; + } + + private static JSONObject getMediaItemJson(MediaItem item) throws JSONException { + JSONObject json = new JSONObject(); + json.put(KEY_URI, item.uri.toString()); + json.put(KEY_TITLE, item.title); + json.put(KEY_MIME_TYPE, item.mimeType); + if (item.drmConfiguration != null) { + json.put(KEY_DRM_CONFIGURATION, getDrmConfigurationJson(item.drmConfiguration)); + } + return json; + } + + private static JSONObject getDrmConfigurationJson(DrmConfiguration drmConfiguration) + throws JSONException { + JSONObject json = new JSONObject(); + json.put(KEY_UUID, drmConfiguration.uuid); + json.put(KEY_LICENSE_URI, drmConfiguration.licenseUri); + json.put(KEY_REQUEST_HEADERS, new JSONObject(drmConfiguration.requestHeaders)); + return json; + } + + @Nullable + private static JSONObject getPlayerConfigJson(MediaItem item) throws JSONException { + DrmConfiguration drmConfiguration = item.drmConfiguration; if (drmConfiguration == null) { - return customData; + return null; } String drmScheme; @@ -58,24 +149,19 @@ public final class DefaultMediaItemConverter implements MediaItemConverter { } else if (C.PLAYREADY_UUID.equals(drmConfiguration.uuid)) { drmScheme = "playready"; } else { - return customData; + return null; } - JSONObject exoPlayerConfig = new JSONObject(); - try { - exoPlayerConfig.put("withCredentials", false); - exoPlayerConfig.put("protectionSystem", drmScheme); - if (drmConfiguration.licenseUri != null) { - exoPlayerConfig.put("licenseUrl", drmConfiguration.licenseUri); - } - if (!drmConfiguration.requestHeaders.isEmpty()) { - exoPlayerConfig.put("headers", new JSONObject(drmConfiguration.requestHeaders)); - } - customData.put("exoPlayerConfig", exoPlayerConfig); - } catch (JSONException e) { - throw new RuntimeException(e); + JSONObject exoPlayerConfigJson = new JSONObject(); + exoPlayerConfigJson.put("withCredentials", false); + exoPlayerConfigJson.put("protectionSystem", drmScheme); + if (drmConfiguration.licenseUri != null) { + exoPlayerConfigJson.put("licenseUrl", drmConfiguration.licenseUri); + } + if (!drmConfiguration.requestHeaders.isEmpty()) { + exoPlayerConfigJson.put("headers", new JSONObject(drmConfiguration.requestHeaders)); } - return customData; + return exoPlayerConfigJson; } } diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java index 3cb2540ad8..23633aa4d2 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java @@ -28,5 +28,11 @@ public interface MediaItemConverter { */ MediaQueueItem toMediaQueueItem(MediaItem mediaItem); - // TODO: Add toMediaItem to convert in the opposite direction. + /** + * Converts a {@link MediaQueueItem} to a {@link MediaItem}. + * + * @param mediaQueueItem The {@link MediaQueueItem}. + * @return The equivalent {@link MediaItem}. + */ + MediaItem toMediaItem(MediaQueueItem mediaQueueItem); } diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java new file mode 100644 index 0000000000..cf9b9d3496 --- /dev/null +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2019 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.ext.cast; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration; +import com.google.android.gms.cast.MediaQueueItem; +import java.util.Collections; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test for {@link DefaultMediaItemConverter}. */ +@RunWith(AndroidJUnit4.class) +public class DefaultMediaItemConverterTest { + + @Test + public void serialize_deserialize_minimal() { + MediaItem.Builder builder = new MediaItem.Builder(); + MediaItem item = builder.setUri(Uri.parse("http://example.com")).setMimeType("mime").build(); + + DefaultMediaItemConverter converter = new DefaultMediaItemConverter(); + MediaQueueItem queueItem = converter.toMediaQueueItem(item); + MediaItem reconstructedItem = converter.toMediaItem(queueItem); + + assertThat(reconstructedItem).isEqualTo(item); + } + + @Test + public void serialize_deserialize_complete() { + MediaItem.Builder builder = new MediaItem.Builder(); + MediaItem item = + builder + .setUri(Uri.parse("http://example.com")) + .setTitle("title") + .setMimeType("mime") + .setDrmConfiguration( + new DrmConfiguration( + C.WIDEVINE_UUID, + Uri.parse("http://license.com"), + Collections.singletonMap("key", "value"))) + .build(); + + DefaultMediaItemConverter converter = new DefaultMediaItemConverter(); + MediaQueueItem queueItem = converter.toMediaQueueItem(item); + MediaItem reconstructedItem = converter.toMediaItem(queueItem); + + assertThat(reconstructedItem).isEqualTo(item); + } +} From 27a4f96cb17b727935f85e5a8c99e766b0711c72 Mon Sep 17 00:00:00 2001 From: "Venkatarama NG. Avadhani" Date: Tue, 30 Jul 2019 11:47:33 +0530 Subject: [PATCH 1498/1556] Clean up FLAC picture parsing --- .../exoplayer2/ext/flac/FlacExtractor.java | 4 +- extensions/flac/src/main/jni/flac_jni.cc | 48 ++++++++----------- extensions/flac/src/main/jni/flac_parser.cc | 30 +++++++----- .../flac/src/main/jni/include/flac_parser.h | 14 +++--- .../metadata/flac/PictureFrame.java | 8 ++-- .../exoplayer2/util/FlacStreamMetadata.java | 12 ++--- ...PictureTest.java => PictureFrameTest.java} | 2 +- .../util/FlacStreamMetadataTest.java | 8 ++-- .../android/exoplayer2/ui/PlayerView.java | 21 +++++--- 9 files changed, 76 insertions(+), 71 deletions(-) rename library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/{PictureTest.java => PictureFrameTest.java} (97%) diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 9f79f09117..cd91b06288 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -229,8 +229,8 @@ public final class FlacExtractor implements Extractor { binarySearchSeeker = outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput); Metadata metadata = id3MetadataDisabled ? null : id3Metadata; - if (streamMetadata.flacMetadata != null) { - metadata = streamMetadata.flacMetadata.copyWithAppendedEntriesFrom(metadata); + if (streamMetadata.metadata != null) { + metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata); } outputFormat(streamMetadata, metadata, trackOutput); outputBuffer.reset(streamMetadata.maxDecodedFrameSize()); diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 4ccd24781b..9cc611559a 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -117,24 +117,24 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { } } - jobject pictureList = env->NewObject(arrayListClass, arrayListConstructor); - bool picValid = context->parser->isPicValid(); - if (picValid) { - std::vector pictures = context->parser->getPictures(); - jclass flacPictureFrameClass = env->FindClass( + jobject jPictures = env->NewObject(arrayListClass, arrayListConstructor); + bool picturesValid = context->parser->arePicturesValid(); + if (picturesValid) { + std::vector pictures = context->parser->getPictures(); + jclass pictureFrameClass = env->FindClass( "com/google/android/exoplayer2/metadata/flac/PictureFrame"); - jmethodID flacPictureFrameConstructor = env->GetMethodID( - flacPictureFrameClass, "", + jmethodID pictureFrameConstructor = env->GetMethodID( + pictureFrameClass, "", "(ILjava/lang/String;Ljava/lang/String;IIII[B)V"); - for (std::vector::const_iterator picture = pictures.begin(); + for (std::vector::const_iterator picture = pictures.begin(); picture != pictures.end(); ++picture) { jstring mimeType = env->NewStringUTF(picture->mimeType.c_str()); jstring description = env->NewStringUTF(picture->description.c_str()); - jbyteArray picArr = env->NewByteArray(picture->data.size()); - env->SetByteArrayRegion(picArr, 0, picture->data.size(), + jbyteArray pictureData = env->NewByteArray(picture->data.size()); + env->SetByteArrayRegion(pictureData, 0, picture->data.size(), (signed char *)&picture->data[0]); - jobject flacPictureFrame = env->NewObject(flacPictureFrameClass, - flacPictureFrameConstructor, + jobject pictureFrame = env->NewObject(pictureFrameClass, + pictureFrameConstructor, picture->type, mimeType, description, @@ -142,11 +142,11 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { picture->height, picture->depth, picture->colors, - picArr); - env->CallBooleanMethod(pictureList, arrayListAddMethod, flacPictureFrame); + pictureData); + env->CallBooleanMethod(jPictures, arrayListAddMethod, pictureFrame); env->DeleteLocalRef(mimeType); env->DeleteLocalRef(description); - env->DeleteLocalRef(picArr); + env->DeleteLocalRef(pictureData); } } @@ -160,18 +160,12 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { flacStreamMetadataClass, "", "(IIIIIIIJLjava/util/List;Ljava/util/List;)V"); - jobject streamMetaData = env->NewObject(flacStreamMetadataClass, - flacStreamMetadataConstructor, - streamInfo.min_blocksize, - streamInfo.max_blocksize, - streamInfo.min_framesize, - streamInfo.max_framesize, - streamInfo.sample_rate, - streamInfo.channels, - streamInfo.bits_per_sample, - streamInfo.total_samples, - commentList, pictureList); - return streamMetaData; + return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor, + streamInfo.min_blocksize, streamInfo.max_blocksize, + streamInfo.min_framesize, streamInfo.max_framesize, + streamInfo.sample_rate, streamInfo.channels, + streamInfo.bits_per_sample, streamInfo.total_samples, + commentList, jPictures); } DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) { diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index fafc254482..b9e5cace71 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -193,18 +193,22 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) { break; case FLAC__METADATA_TYPE_PICTURE: { - const FLAC__StreamMetadata_Picture *pic = &metadata->data.picture; - flacPicture picture; - picture.mimeType.assign(std::string(pic->mime_type)); - picture.description.assign(std::string((char *)pic->description)); - picture.data.assign(pic->data, pic->data + pic->data_length); - picture.width = pic->width; - picture.height = pic->height; - picture.depth = pic->depth; - picture.colors = pic->colors; - picture.type = pic->type; - mPictures.push_back(picture); - mPicValid = true; + const FLAC__StreamMetadata_Picture *parsedPicture = + &metadata->data.picture; + FlacPicture flacPicture; + flacPicture.mimeType.assign(std::string(parsedPicture->mime_type)); + flacPicture.description.assign( + std::string((char *)parsedPicture->description)); + flacPicture.data.assign( + parsedPicture->data, + parsedPicture->data + parsedPicture->data_length); + flacPicture.width = parsedPicture->width; + flacPicture.height = parsedPicture->height; + flacPicture.depth = parsedPicture->depth; + flacPicture.colors = parsedPicture->colors; + flacPicture.type = parsedPicture->type; + mPictures.push_back(flacPicture); + mPicturesValid = true; break; } default: @@ -269,7 +273,7 @@ FLACParser::FLACParser(DataSource *source) mEOF(false), mStreamInfoValid(false), mVorbisCommentsValid(false), - mPicValid(false), + mPicturesValid(false), mWriteRequested(false), mWriteCompleted(false), mWriteBuffer(NULL), diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index f1d175b94f..9c6452c160 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -30,7 +30,7 @@ typedef int status_t; -typedef struct { +struct FlacPicture { int type; std::string mimeType; std::string description; @@ -39,7 +39,7 @@ typedef struct { FLAC__uint32 depth; FLAC__uint32 colors; std::vector data; -} flacPicture; +}; class FLACParser { public: @@ -65,9 +65,9 @@ class FLACParser { return mVorbisComments; } - bool isPicValid() const { return mPicValid; } + bool arePicturesValid() const { return mPicturesValid; } - const std::vector& getPictures() const { return mPictures; } + const std::vector& getPictures() const { return mPictures; } int64_t getLastFrameTimestamp() const { return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); @@ -97,7 +97,7 @@ class FLACParser { if (newPosition == 0) { mStreamInfoValid = false; mVorbisCommentsValid = false; - mPicValid = false; + mPicturesValid = false; mVorbisComments.clear(); mPictures.clear(); FLAC__stream_decoder_reset(mDecoder); @@ -150,8 +150,8 @@ class FLACParser { bool mVorbisCommentsValid; // cached when the PICTURE metadata is parsed by libFLAC - std::vector mPictures; - bool mPicValid; + std::vector mPictures; + bool mPicturesValid; // cached when a decoded PCM block is "written" by libFLAC parser bool mWriteRequested; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java index fcf1fd6e58..dc280be9ff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java @@ -31,7 +31,7 @@ public final class PictureFrame implements Metadata.Entry { /** The mime type of the picture. */ public final String mimeType; /** A description of the picture. */ - @Nullable public final String description; + public final String description; /** The pixel width of the picture. */ public final int width; /** The pixel height of the picture. */ @@ -49,7 +49,7 @@ public final class PictureFrame implements Metadata.Entry { public PictureFrame( int pictureType, String mimeType, - @Nullable String description, + String description, int width, int height, int depth, @@ -111,8 +111,8 @@ public final class PictureFrame implements Metadata.Entry { public int hashCode() { int result = 17; result = 31 * result + pictureType; - result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); - result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + mimeType.hashCode(); + result = 31 * result + description.hashCode(); result = 31 * result + width; result = 31 * result + height; result = 31 * result + depth; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java index 48680b5095..e7851aa0a4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java @@ -36,7 +36,7 @@ public final class FlacStreamMetadata { public final int channels; public final int bitsPerSample; public final long totalSamples; - @Nullable public final Metadata flacMetadata; + @Nullable public final Metadata metadata; private static final String SEPARATOR = "="; @@ -59,7 +59,7 @@ public final class FlacStreamMetadata { this.channels = scratch.readBits(3) + 1; this.bitsPerSample = scratch.readBits(5) + 1; this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL); - this.flacMetadata = null; + this.metadata = null; } /** @@ -72,7 +72,7 @@ public final class FlacStreamMetadata { * @param bitsPerSample Number of bits per sample of the FLAC stream. * @param totalSamples Total samples of the FLAC stream. * @param vorbisComments Vorbis comments. Each entry must be in key=value form. - * @param pictureList A list of pictures in the stream. + * @param pictures A list of pictures in the stream. * @see FLAC format * METADATA_BLOCK_STREAMINFO * @see FLAC format @@ -90,7 +90,7 @@ public final class FlacStreamMetadata { int bitsPerSample, long totalSamples, List vorbisComments, - List pictureList) { + List pictures) { this.minBlockSize = minBlockSize; this.maxBlockSize = maxBlockSize; this.minFrameSize = minFrameSize; @@ -99,8 +99,8 @@ public final class FlacStreamMetadata { this.channels = channels; this.bitsPerSample = bitsPerSample; this.totalSamples = totalSamples; - Metadata metadata = new Metadata(pictureList); - this.flacMetadata = metadata.copyWithAppendedEntriesFrom(parseVorbisComments(vorbisComments)); + this.metadata = + new Metadata(pictures).copyWithAppendedEntriesFrom(parseVorbisComments(vorbisComments)); } /** Returns the maximum size for a decoded frame from the FLAC stream. */ diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java similarity index 97% rename from library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java index 04a5b46e26..4263103eeb 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java @@ -24,7 +24,7 @@ import org.junit.runner.RunWith; /** Test for {@link PictureFrame}. */ @RunWith(AndroidJUnit4.class) -public class PictureTest { +public final class PictureFrameTest { @Test public void testParcelable() { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java index c556282ca2..3988e5e45e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java @@ -36,7 +36,7 @@ public final class FlacStreamMetadataTest { Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()) - .flacMetadata; + .metadata; assertThat(metadata.length()).isEqualTo(2); VorbisComment commentFrame = (VorbisComment) metadata.get(0); @@ -53,7 +53,7 @@ public final class FlacStreamMetadataTest { Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()) - .flacMetadata; + .metadata; assertThat(metadata).isNull(); } @@ -65,7 +65,7 @@ public final class FlacStreamMetadataTest { Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()) - .flacMetadata; + .metadata; assertThat(metadata.length()).isEqualTo(1); VorbisComment commentFrame = (VorbisComment) metadata.get(0); @@ -81,7 +81,7 @@ public final class FlacStreamMetadataTest { Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()) - .flacMetadata; + .metadata; assertThat(metadata.length()).isEqualTo(1); VorbisComment commentFrame = (VorbisComment) metadata.get(0); 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 4ac007fa55..1abdd33bb2 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 @@ -305,6 +305,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider private int textureViewRotation; private boolean isTouching; private static final int PICTURE_TYPE_FRONT_COVER = 3; + private static final int PICTURE_TYPE_NOT_SET = -1; public PlayerView(Context context) { this(context, null); @@ -1249,25 +1250,31 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider private boolean setArtworkFromMetadata(Metadata metadata) { boolean isArtworkSet = false; - int currentPicType = -1; + int currentPictureType = PICTURE_TYPE_NOT_SET; for (int i = 0; i < metadata.length(); i++) { Metadata.Entry metadataEntry = metadata.get(i); - int picType; + int pictureType; byte[] bitmapData; if (metadataEntry instanceof ApicFrame) { bitmapData = ((ApicFrame) metadataEntry).pictureData; - picType = ((ApicFrame) metadataEntry).pictureType; + pictureType = ((ApicFrame) metadataEntry).pictureType; } else if (metadataEntry instanceof PictureFrame) { bitmapData = ((PictureFrame) metadataEntry).pictureData; - picType = ((PictureFrame) metadataEntry).pictureType; + pictureType = ((PictureFrame) metadataEntry).pictureType; } else { continue; } - /* Prefers the first front cover picture in the picture list */ - if (currentPicType != PICTURE_TYPE_FRONT_COVER) { + /* Prefers the first front cover picture. + * If there are no front cover pictures, prefer the first picture in the list + * */ + if (currentPictureType == PICTURE_TYPE_NOT_SET || pictureType == PICTURE_TYPE_FRONT_COVER) { Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length); isArtworkSet = setDrawableArtwork(new BitmapDrawable(getResources(), bitmap)); - currentPicType = picType; + currentPictureType = pictureType; + if (currentPictureType == PICTURE_TYPE_FRONT_COVER) { + /* Found a front cover, stop looking for more pictures. */ + break; + } } } return isArtworkSet; From 58006ac3adf61504b83f4ac879ab2c663d0037ec Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 29 Jul 2019 22:41:58 +0100 Subject: [PATCH 1499/1556] Tweak Firebase JobDispatcher extension README PiperOrigin-RevId: 260583198 --- extensions/jobdispatcher/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md index bd76868625..a6f0c3966a 100644 --- a/extensions/jobdispatcher/README.md +++ b/extensions/jobdispatcher/README.md @@ -1,11 +1,11 @@ # ExoPlayer Firebase JobDispatcher extension # -**DEPRECATED** Please use [WorkManager extension][] or [`PlatformScheduler`]. +**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][] instead.** This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][]. [WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md -[`PlatformScheduler`]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java +[PlatformScheduler]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java [Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android ## Getting the extension ## @@ -24,4 +24,3 @@ locally. Instructions for doing this can be found in ExoPlayer's [top level README][]. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md - From 40926618ad887b72221e2301ab8e7118925cbcb1 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 30 Jul 2019 11:21:52 +0100 Subject: [PATCH 1500/1556] Return the removed media source from ConcatenatingMediaSource.removeMediaSource PiperOrigin-RevId: 260681773 --- .../exoplayer2/source/ConcatenatingMediaSource.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 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 669a0e7bb4..8dfea1e511 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 @@ -263,9 +263,12 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource Date: Tue, 30 Jul 2019 11:32:40 +0100 Subject: [PATCH 1501/1556] Make blocking fixed track bandwidth the default and remove experimental flag. PiperOrigin-RevId: 260682878 --- .../AdaptiveTrackSelection.java | 76 +++++++------------ .../AdaptiveTrackSelectionTest.java | 3 + 2 files changed, 32 insertions(+), 47 deletions(-) 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 ca8a0b12f9..c5d22c15cb 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 @@ -49,7 +49,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final Clock clock; private TrackBitrateEstimator trackBitrateEstimator; - private boolean blockFixedTrackSelectionBandwidth; /** Creates an adaptive track selection factory with default parameters. */ public Factory() { @@ -218,15 +217,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this.trackBitrateEstimator = trackBitrateEstimator; } - /** - * Enables blocking of the total fixed track selection bandwidth. - * - *

          This method is experimental, and will be renamed or removed in a future release. - */ - public final void experimental_enableBlockFixedTrackSelectionBandwidth() { - this.blockFixedTrackSelectionBandwidth = true; - } - @Override public final @NullableType TrackSelection[] createTrackSelections( @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { @@ -234,20 +224,11 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { bandwidthMeter = this.bandwidthMeter; } TrackSelection[] selections = new TrackSelection[definitions.length]; - List adaptiveSelections = new ArrayList<>(); int totalFixedBandwidth = 0; for (int i = 0; i < definitions.length; i++) { Definition definition = definitions[i]; - if (definition == null) { - continue; - } - if (definition.tracks.length > 1) { - AdaptiveTrackSelection adaptiveSelection = - createAdaptiveTrackSelection(definition.group, bandwidthMeter, definition.tracks); - adaptiveSelection.experimental_setTrackBitrateEstimator(trackBitrateEstimator); - adaptiveSelections.add(adaptiveSelection); - selections[i] = adaptiveSelection; - } else { + if (definition != null && definition.tracks.length == 1) { + // Make fixed selections first to know their total bandwidth. selections[i] = new FixedTrackSelection( definition.group, definition.tracks[0], definition.reason, definition.data); @@ -257,9 +238,16 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { } } } - if (blockFixedTrackSelectionBandwidth) { - for (int i = 0; i < adaptiveSelections.size(); i++) { - adaptiveSelections.get(i).experimental_setNonAllocatableBandwidth(totalFixedBandwidth); + List adaptiveSelections = new ArrayList<>(); + for (int i = 0; i < definitions.length; i++) { + Definition definition = definitions[i]; + if (definition != null && definition.tracks.length > 1) { + AdaptiveTrackSelection adaptiveSelection = + createAdaptiveTrackSelection( + definition.group, bandwidthMeter, definition.tracks, totalFixedBandwidth); + adaptiveSelection.experimental_setTrackBitrateEstimator(trackBitrateEstimator); + adaptiveSelections.add(adaptiveSelection); + selections[i] = adaptiveSelection; } } if (adaptiveSelections.size() > 1) { @@ -288,14 +276,19 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { * @param group The {@link TrackGroup}. * @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks. * @param tracks The indices of the selected tracks in the track group. + * @param totalFixedTrackBandwidth The total bandwidth used by all non-adaptive tracks, in bits + * per second. * @return An {@link AdaptiveTrackSelection} for the specified tracks. */ protected AdaptiveTrackSelection createAdaptiveTrackSelection( - TrackGroup group, BandwidthMeter bandwidthMeter, int[] tracks) { + TrackGroup group, + BandwidthMeter bandwidthMeter, + int[] tracks, + int totalFixedTrackBandwidth) { return new AdaptiveTrackSelection( group, tracks, - new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction), + new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, totalFixedTrackBandwidth), minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, @@ -341,6 +334,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { group, tracks, bandwidthMeter, + /* reservedBandwidth= */ 0, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, @@ -355,6 +349,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be * empty. May be in any order. * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + * @param reservedBandwidth The reserved bandwidth, which shouldn't be considered available for + * use, in bits per second. * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the * selected track to switch to one of higher quality. * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the @@ -381,6 +377,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, + long reservedBandwidth, long minDurationForQualityIncreaseMs, long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, @@ -391,7 +388,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this( group, tracks, - new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction), + new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, reservedBandwidth), minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, @@ -445,18 +442,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this.trackBitrateEstimator = trackBitrateEstimator; } - /** - * Sets the non-allocatable bandwidth, which shouldn't be considered available. - * - *

          This method is experimental, and will be renamed or removed in a future release. - * - * @param nonAllocatableBandwidth The non-allocatable bandwidth in bits per second. - */ - public void experimental_setNonAllocatableBandwidth(long nonAllocatableBandwidth) { - ((DefaultBandwidthProvider) bandwidthProvider) - .experimental_setNonAllocatableBandwidth(nonAllocatableBandwidth); - } - /** * Sets checkpoints to determine the allocation bandwidth based on the total bandwidth. * @@ -666,20 +651,21 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final BandwidthMeter bandwidthMeter; private final float bandwidthFraction; - - private long nonAllocatableBandwidth; + private final long reservedBandwidth; @Nullable private long[][] allocationCheckpoints; - /* package */ DefaultBandwidthProvider(BandwidthMeter bandwidthMeter, float bandwidthFraction) { + /* package */ DefaultBandwidthProvider( + BandwidthMeter bandwidthMeter, float bandwidthFraction, long reservedBandwidth) { this.bandwidthMeter = bandwidthMeter; this.bandwidthFraction = bandwidthFraction; + this.reservedBandwidth = reservedBandwidth; } @Override public long getAllocatedBandwidth() { long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction); - long allocatableBandwidth = Math.max(0L, totalBandwidth - nonAllocatableBandwidth); + long allocatableBandwidth = Math.max(0L, totalBandwidth - reservedBandwidth); if (allocationCheckpoints == null) { return allocatableBandwidth; } @@ -695,10 +681,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { return previous[1] + (long) (fractionBetweenCheckpoints * (next[1] - previous[1])); } - /* package */ void experimental_setNonAllocatableBandwidth(long nonAllocatableBandwidth) { - this.nonAllocatableBandwidth = nonAllocatableBandwidth; - } - /* package */ void experimental_setBandwidthAllocationCheckpoints( long[][] allocationCheckpoints) { Assertions.checkArgument(allocationCheckpoints.length >= 2); 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 91e7393fe7..456f7f7107 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 @@ -392,6 +392,7 @@ public final class AdaptiveTrackSelectionTest { trackGroup, selectedAllTracksInGroup(trackGroup), mockBandwidthMeter, + /* reservedBandwidth= */ 0, minDurationForQualityIncreaseMs, AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, @@ -408,6 +409,7 @@ public final class AdaptiveTrackSelectionTest { trackGroup, selectedAllTracksInGroup(trackGroup), mockBandwidthMeter, + /* reservedBandwidth= */ 0, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, maxDurationForQualityDecreaseMs, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, @@ -426,6 +428,7 @@ public final class AdaptiveTrackSelectionTest { trackGroup, selectedAllTracksInGroup(trackGroup), mockBandwidthMeter, + /* reservedBandwidth= */ 0, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, durationToRetainAfterDiscardMs, From ce2e2797cb0a91496c27ee8650a90c064c05e935 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 30 Jul 2019 12:47:31 +0100 Subject: [PATCH 1502/1556] Improve DefaultMediaClock behaviour. DefaultMediaClock has currently two non-ideal behaviours: 1. One part of checking if it should use the renderer clock is checking whether the associated renderer finished reading its stream. This only makes sense if the renderer isn't already reading ahead into the next period. This can be solved by forwarding if we are reading ahead to the sync command. 2. When switching from stand-alone to renderer clock we assume they are exactly at the same position. This is true in theory, but in practise there may be small differences due to the different natures of these clocks. To prevent jumping backwards in time, we can temporarily stop the stand-alone clock and only switch once the renderer clock reached the same position. PiperOrigin-RevId: 260690468 --- .../android/exoplayer2/DefaultMediaClock.java | 88 ++++++++++++------- .../exoplayer2/ExoPlayerImplInternal.java | 4 +- .../exoplayer2/DefaultMediaClockTest.java | 79 +++++++++++------ 3 files changed, 111 insertions(+), 60 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java index 410dffd558..1971a4cefc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java @@ -40,11 +40,13 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters); } - private final StandaloneMediaClock standaloneMediaClock; + private final StandaloneMediaClock standaloneClock; private final PlaybackParameterListener listener; @Nullable private Renderer rendererClockSource; @Nullable private MediaClock rendererClock; + private boolean isUsingStandaloneClock; + private boolean standaloneClockIsStarted; /** * Creates a new instance with listener for playback parameter changes and a {@link Clock} to use @@ -56,21 +58,24 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; */ public DefaultMediaClock(PlaybackParameterListener listener, Clock clock) { this.listener = listener; - this.standaloneMediaClock = new StandaloneMediaClock(clock); + this.standaloneClock = new StandaloneMediaClock(clock); + isUsingStandaloneClock = true; } /** * Starts the standalone fallback clock. */ public void start() { - standaloneMediaClock.start(); + standaloneClockIsStarted = true; + standaloneClock.start(); } /** * Stops the standalone fallback clock. */ public void stop() { - standaloneMediaClock.stop(); + standaloneClockIsStarted = false; + standaloneClock.stop(); } /** @@ -79,7 +84,7 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; * @param positionUs The position to set in microseconds. */ public void resetPosition(long positionUs) { - standaloneMediaClock.resetPosition(positionUs); + standaloneClock.resetPosition(positionUs); } /** @@ -99,8 +104,7 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; } this.rendererClock = rendererMediaClock; this.rendererClockSource = renderer; - rendererClock.setPlaybackParameters(standaloneMediaClock.getPlaybackParameters()); - ensureSynced(); + rendererClock.setPlaybackParameters(standaloneClock.getPlaybackParameters()); } } @@ -114,30 +118,25 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; if (renderer == rendererClockSource) { this.rendererClock = null; this.rendererClockSource = null; + isUsingStandaloneClock = true; } } /** * Syncs internal clock if needed and returns current clock position in microseconds. + * + * @param isReadingAhead Whether the renderers are reading ahead. */ - public long syncAndGetPositionUs() { - if (isUsingRendererClock()) { - ensureSynced(); - return rendererClock.getPositionUs(); - } else { - return standaloneMediaClock.getPositionUs(); - } + public long syncAndGetPositionUs(boolean isReadingAhead) { + syncClocks(isReadingAhead); + return getPositionUs(); } // MediaClock implementation. @Override public long getPositionUs() { - if (isUsingRendererClock()) { - return rendererClock.getPositionUs(); - } else { - return standaloneMediaClock.getPositionUs(); - } + return isUsingStandaloneClock ? standaloneClock.getPositionUs() : rendererClock.getPositionUs(); } @Override @@ -146,32 +145,53 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; rendererClock.setPlaybackParameters(playbackParameters); playbackParameters = rendererClock.getPlaybackParameters(); } - standaloneMediaClock.setPlaybackParameters(playbackParameters); + standaloneClock.setPlaybackParameters(playbackParameters); } @Override public PlaybackParameters getPlaybackParameters() { - return rendererClock != null ? rendererClock.getPlaybackParameters() - : standaloneMediaClock.getPlaybackParameters(); + return rendererClock != null + ? rendererClock.getPlaybackParameters() + : standaloneClock.getPlaybackParameters(); } - private void ensureSynced() { + private void syncClocks(boolean isReadingAhead) { + if (shouldUseStandaloneClock(isReadingAhead)) { + isUsingStandaloneClock = true; + if (standaloneClockIsStarted) { + standaloneClock.start(); + } + return; + } long rendererClockPositionUs = rendererClock.getPositionUs(); - standaloneMediaClock.resetPosition(rendererClockPositionUs); + if (isUsingStandaloneClock) { + // Ensure enabling the renderer clock doesn't jump backwards in time. + if (rendererClockPositionUs < standaloneClock.getPositionUs()) { + standaloneClock.stop(); + return; + } + isUsingStandaloneClock = false; + if (standaloneClockIsStarted) { + standaloneClock.start(); + } + } + // Continuously sync stand-alone clock to renderer clock so that it can take over if needed. + standaloneClock.resetPosition(rendererClockPositionUs); PlaybackParameters playbackParameters = rendererClock.getPlaybackParameters(); - if (!playbackParameters.equals(standaloneMediaClock.getPlaybackParameters())) { - standaloneMediaClock.setPlaybackParameters(playbackParameters); + if (!playbackParameters.equals(standaloneClock.getPlaybackParameters())) { + standaloneClock.setPlaybackParameters(playbackParameters); listener.onPlaybackParametersChanged(playbackParameters); } } - private boolean isUsingRendererClock() { - // Use the renderer clock if the providing renderer has not ended or needs the next sample - // stream to reenter the ready state. The latter case uses the standalone clock to avoid getting - // stuck if tracks in the current period have uneven durations. - // See: https://github.com/google/ExoPlayer/issues/1874. - return rendererClockSource != null && !rendererClockSource.isEnded() - && (rendererClockSource.isReady() || !rendererClockSource.hasReadStreamToEnd()); + private boolean shouldUseStandaloneClock(boolean isReadingAhead) { + // Use the standalone clock if the clock providing renderer is not set or has ended. Also use + // the standalone clock if the renderer is not ready and we have finished reading the stream or + // are reading ahead to avoid getting stuck if tracks in the current period have uneven + // durations. See: https://github.com/google/ExoPlayer/issues/1874. + return rendererClockSource == null + || rendererClockSource.isEnded() + || (!rendererClockSource.isReady() + && (isReadingAhead || rendererClockSource.hasReadStreamToEnd())); } - } 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 b6317941fb..488d002ab2 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 @@ -535,7 +535,9 @@ import java.util.concurrent.atomic.AtomicBoolean; playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } } else { - rendererPositionUs = mediaClock.syncAndGetPositionUs(); + rendererPositionUs = + mediaClock.syncAndGetPositionUs( + /* isReadingAhead= */ playingPeriodHolder != queue.getReadingPeriod()); periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); maybeTriggerPendingMessages(playbackInfo.positionUs, periodPositionUs); playbackInfo.positionUs = periodPositionUs; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java index c42edb32ae..b6e3d7a648 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java @@ -53,13 +53,14 @@ public class DefaultMediaClockTest { @Test public void standaloneResetPosition_getPositionShouldReturnSameValue() throws Exception { mediaClock.resetPosition(TEST_POSITION_US); - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) + .isEqualTo(TEST_POSITION_US); } @Test public void standaloneGetAndResetPosition_shouldNotTriggerCallback() throws Exception { mediaClock.resetPosition(TEST_POSITION_US); - mediaClock.syncAndGetPositionUs(); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); verifyNoMoreInteractions(listener); } @@ -77,7 +78,7 @@ public class DefaultMediaClockTest { @Test public void standaloneStart_shouldStartClock() throws Exception { mediaClock.start(); - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); } @Test @@ -98,7 +99,7 @@ public class DefaultMediaClockTest { mediaClock.start(); mediaClock.stop(); mediaClock.start(); - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); } @Test @@ -130,7 +131,7 @@ public class DefaultMediaClockTest { mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); mediaClock.start(); // Asserts that clock is running with speed declared in getPlaybackParameters(). - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); } @Test @@ -165,6 +166,7 @@ public class DefaultMediaClockTest { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); } @@ -174,6 +176,7 @@ public class DefaultMediaClockTest { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); verifyNoMoreInteractions(listener); } @@ -183,7 +186,9 @@ public class DefaultMediaClockTest { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); mediaClock.onRendererDisabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); } @@ -193,6 +198,7 @@ public class DefaultMediaClockTest { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); } @@ -203,6 +209,7 @@ public class DefaultMediaClockTest { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); verifyNoMoreInteractions(listener); } @@ -213,6 +220,7 @@ public class DefaultMediaClockTest { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); } @@ -223,7 +231,8 @@ public class DefaultMediaClockTest { mediaClock.start(); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClockRenderer.positionUs = TEST_POSITION_US; - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) + .isEqualTo(TEST_POSITION_US); // We're not advancing the renderer media clock. Thus, the clock should appear to be stopped. assertClockIsStopped(); } @@ -235,9 +244,11 @@ public class DefaultMediaClockTest { mediaClock.start(); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClockRenderer.positionUs = TEST_POSITION_US; - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) + .isEqualTo(TEST_POSITION_US); mediaClock.resetPosition(0); - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) + .isEqualTo(TEST_POSITION_US); } @Test @@ -246,23 +257,24 @@ public class DefaultMediaClockTest { mediaClock.start(); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClockRenderer.positionUs = TEST_POSITION_US; - mediaClock.syncAndGetPositionUs(); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); mediaClock.onRendererDisabled(mediaClockRenderer); fakeClock.advanceTime(SLEEP_TIME_MS); - assertThat(mediaClock.syncAndGetPositionUs()) + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) .isEqualTo(TEST_POSITION_US + C.msToUs(SLEEP_TIME_MS)); - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); } @Test public void getPositionWithPlaybackParameterChange_shouldTriggerCallback() throws ExoPlaybackException { - MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, - /* playbackParametersAreMutable= */ true); + MediaClockRenderer mediaClockRenderer = + new MediaClockRenderer( + PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); // Silently change playback parameters of renderer clock. mediaClockRenderer.playbackParameters = TEST_PLAYBACK_PARAMETERS; - mediaClock.syncAndGetPositionUs(); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); } @@ -283,7 +295,18 @@ public class DefaultMediaClockTest { /* isEnded= */ false, /* hasReadStreamToEnd= */ true); mediaClock.start(); mediaClock.onRendererEnabled(mediaClockRenderer); - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); + } + + @Test + public void rendererNotReadyAndReadingAhead_shouldFallbackToStandaloneClock() + throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = + new MediaClockRenderer( + /* isReady= */ false, /* isEnded= */ false, /* hasReadStreamToEnd= */ false); + mediaClock.start(); + mediaClock.onRendererEnabled(mediaClockRenderer); + assertClockIsRunning(/* isReadingAhead= */ true); } @Test @@ -293,7 +316,7 @@ public class DefaultMediaClockTest { /* isEnded= */ true, /* hasReadStreamToEnd= */ true); mediaClock.start(); mediaClock.onRendererEnabled(mediaClockRenderer); - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); } @Test @@ -302,7 +325,8 @@ public class DefaultMediaClockTest { MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(); mediaClockRenderer.positionUs = TEST_POSITION_US; mediaClock.onRendererDisabled(mediaClockRenderer); - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(C.msToUs(fakeClock.elapsedRealtime())); + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) + .isEqualTo(C.msToUs(fakeClock.elapsedRealtime())); } @Test @@ -312,7 +336,8 @@ public class DefaultMediaClockTest { mediaClock.onRendererEnabled(mediaClockRenderer); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClockRenderer.positionUs = TEST_POSITION_US; - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) + .isEqualTo(TEST_POSITION_US); } @Test @@ -328,20 +353,24 @@ public class DefaultMediaClockTest { } catch (ExoPlaybackException e) { // Expected. } - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) + .isEqualTo(TEST_POSITION_US); } - private void assertClockIsRunning() { - long clockStartUs = mediaClock.syncAndGetPositionUs(); + private void assertClockIsRunning(boolean isReadingAhead) { + long clockStartUs = mediaClock.syncAndGetPositionUs(isReadingAhead); fakeClock.advanceTime(SLEEP_TIME_MS); - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(clockStartUs - + mediaClock.getPlaybackParameters().getMediaTimeUsForPlayoutTimeMs(SLEEP_TIME_MS)); + assertThat(mediaClock.syncAndGetPositionUs(isReadingAhead)) + .isEqualTo( + clockStartUs + + mediaClock.getPlaybackParameters().getMediaTimeUsForPlayoutTimeMs(SLEEP_TIME_MS)); } private void assertClockIsStopped() { - long positionAtStartUs = mediaClock.syncAndGetPositionUs(); + long positionAtStartUs = mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); fakeClock.advanceTime(SLEEP_TIME_MS); - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(positionAtStartUs); + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) + .isEqualTo(positionAtStartUs); } @SuppressWarnings("HidingField") From 78350cd17dd2dd03441267a2cbe3df0fe6a614c5 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 30 Jul 2019 16:14:06 +0100 Subject: [PATCH 1503/1556] Update javadoc for TrackOutput#sampleData to make it more clear that implementors aren't expected to rewind with setPosition() PiperOrigin-RevId: 260718614 --- .../com/google/android/exoplayer2/extractor/TrackOutput.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java index d7a1c75302..0d5a168197 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java @@ -119,7 +119,7 @@ public interface TrackOutput { * Called to write sample data to the output. * * @param data A {@link ParsableByteArray} from which to read the sample data. - * @param length The number of bytes to read. + * @param length The number of bytes to read, starting from {@code data.getPosition()}. */ void sampleData(ParsableByteArray data, int length); From 6f2e24915d98a869f9ea360a1aa967bf8fcfed4b Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 31 Jul 2019 11:16:48 +0100 Subject: [PATCH 1504/1556] Add @NonNullApi and annotate two packages with it. This new annotation declares everything as non-null by default and can be used as a package annotation in package-info.java. In this change the core lib offline package and the mediasession extension is annotated that way as initial example usage. PiperOrigin-RevId: 260894548 --- constants.gradle | 1 + .../ext/mediasession/package-info.java | 19 ++++++++++ library/core/build.gradle | 3 ++ .../exoplayer2/offline/package-info.java | 19 ++++++++++ .../android/exoplayer2/util/NonNullApi.java | 36 +++++++++++++++++++ 5 files changed, 78 insertions(+) create mode 100644 extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java diff --git a/constants.gradle b/constants.gradle index aba52817bc..b1c2c636c7 100644 --- a/constants.gradle +++ b/constants.gradle @@ -24,6 +24,7 @@ project.ext { autoValueVersion = '1.6' autoServiceVersion = '1.0-rc4' checkerframeworkVersion = '2.5.0' + jsr305Version = '3.0.2' androidXTestVersion = '1.1.0' truthVersion = '0.44' modulePrefix = ':' diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/package-info.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/package-info.java new file mode 100644 index 0000000000..65c0ce080e --- /dev/null +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.ext.mediasession; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/build.gradle b/library/core/build.gradle index f532ae0e6a..ecb81c4450 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -59,8 +59,11 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.0.2' + compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion + // Uncomment to enable Kotlin non-null strict mode. See [internal: b/138703808]. + // compileOnly "org.jetbrains.kotlin:kotlin-annotations-jvm:1.1.60" androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/package-info.java new file mode 100644 index 0000000000..61450c9cfd --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.offline; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java b/library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java new file mode 100644 index 0000000000..bd7a70eba0 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2019 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.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import javax.annotation.Nonnull; +import javax.annotation.meta.TypeQualifierDefault; +// import kotlin.annotations.jvm.MigrationStatus; +// import kotlin.annotations.jvm.UnderMigration; + +/** + * Annotation to declare all type usages in the annotated instance as {@link Nonnull}, unless + * explicitly marked with a nullable annotation. + */ +@Nonnull +@TypeQualifierDefault(ElementType.TYPE_USE) +// TODO(internal: b/138703808): Uncomment to ensure Kotlin issues compiler errors when non-null +// types are used incorrectly. +// @UnderMigration(status = MigrationStatus.STRICT) +@Retention(RetentionPolicy.CLASS) +public @interface NonNullApi {} From af8f67c0686ecd6c12562aa55f0b4dd8edc8751b Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 31 Jul 2019 13:27:57 +0100 Subject: [PATCH 1505/1556] Don't print warning when skipping RIFF and FMT chunks They're not unexpected! PiperOrigin-RevId: 260907687 --- .../exoplayer2/extractor/wav/WavHeaderReader.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 7a6a7e346f..d76d3f37ea 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 @@ -121,13 +121,13 @@ import java.io.IOException; ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); // Skip all chunks until we hit the data header. ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); - final int data = 0x64617461; - while (chunkHeader.id != data) { - Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); + while (chunkHeader.id != WavUtil.DATA_FOURCC) { + if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.FMT_FOURCC) { + Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); + } long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size; // Override size of RIFF chunk, since it describes its size as the entire file. - final int riff = 0x52494646; - if (chunkHeader.id == riff) { + if (chunkHeader.id == WavUtil.RIFF_FOURCC) { bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4; } if (bytesToSkip > Integer.MAX_VALUE) { From 80ab74748d3e8ee0a2c393830a2cc7593704af0a Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 31 Jul 2019 16:44:19 +0100 Subject: [PATCH 1506/1556] Mp3Extractor: Avoid outputting seek frame as a sample This could previously occur when seeking back to position=0 PiperOrigin-RevId: 260933636 --- .../android/exoplayer2/extractor/mp3/Mp3Extractor.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 8f13cfaa11..a448934359 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -116,6 +116,7 @@ public final class Mp3Extractor implements Extractor { private Seeker seeker; private long basisTimeUs; private long samplesRead; + private int firstSamplePosition; private int sampleBytesRemaining; public Mp3Extractor() { @@ -213,6 +214,10 @@ public final class Mp3Extractor implements Extractor { /* selectionFlags= */ 0, /* language= */ null, (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); + firstSamplePosition = (int) input.getPosition(); + } else if (input.getPosition() == 0 && firstSamplePosition != 0) { + // Skip past the seek frame. + input.skipFully(firstSamplePosition); } return readSample(input); } From 288aa52decf6afacf90964f2d3a425c152677fea Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 31 Jul 2019 18:02:21 +0100 Subject: [PATCH 1507/1556] Clean up some Ogg comments & document granulePosition PiperOrigin-RevId: 260947018 --- .../extractor/ogg/DefaultOggSeeker.java | 28 +++++++++---------- .../extractor/ogg/OggPageHeader.java | 14 +++++++--- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index c83662ee83..9700760c49 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -147,12 +147,12 @@ import java.io.IOException; * which it is sensible to just skip pages to the target granule and pre-roll instead of doing * another seek request. * - * @param targetGranule the target granule position to seek to. - * @param input the {@link ExtractorInput} to read from. - * @return the position to seek the {@link ExtractorInput} to for a next call or -(currentGranule + * @param targetGranule The target granule position to seek to. + * @param input The {@link ExtractorInput} to read from. + * @return The position to seek the {@link ExtractorInput} to for a next call or -(currentGranule * + 2) if it's close enough to skip to the target page. - * @throws IOException thrown if reading from the input fails. - * @throws InterruptedException thrown if interrupted while reading from the input. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. */ @VisibleForTesting public long getNextSeekPosition(long targetGranule, ExtractorInput input) @@ -263,8 +263,8 @@ import java.io.IOException; * @param input The {@code ExtractorInput} to skip to the next page. * @param limit The limit up to which the search should take place. * @return Whether the next page was found. - * @throws IOException thrown if peeking/reading from the input fails. - * @throws InterruptedException thrown if interrupted while peeking/reading from the input. + * @throws IOException If peeking/reading from the input fails. + * @throws InterruptedException If interrupted while peeking/reading from the input. */ @VisibleForTesting boolean skipToNextPage(ExtractorInput input, long limit) @@ -321,14 +321,14 @@ import java.io.IOException; * Skips to the position of the start of the page containing the {@code targetGranule} and returns * the granule of the page previous to the target page. * - * @param input the {@link ExtractorInput} to read from. - * @param targetGranule the target granule. - * @param currentGranule the current granule or -1 if it's unknown. - * @return the granule of the prior page or the {@code currentGranule} if there isn't a prior + * @param input The {@link ExtractorInput} to read from. + * @param targetGranule The target granule. + * @param currentGranule The current granule or -1 if it's unknown. + * @return The granule of the prior page or the {@code currentGranule} if there isn't a prior * page. - * @throws ParserException thrown if populating the page header fails. - * @throws IOException thrown if reading from the input fails. - * @throws InterruptedException thrown if interrupted while reading from the input. + * @throws ParserException If populating the page header fails. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. */ @VisibleForTesting long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java index ff32ae3462..c7fb3ff6a2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java @@ -37,7 +37,13 @@ import java.io.IOException; public int revision; public int type; + /** + * The absolute granule position of the page. This is the total number of samples from the start + * of the file up to the end of the page. Samples partially in the page that continue on + * the next page do not count. + */ public long granulePosition; + public long streamSerialNumber; public long pageSequenceNumber; public long pageChecksum; @@ -71,10 +77,10 @@ import java.io.IOException; * Peeks an Ogg page header and updates this {@link OggPageHeader}. * * @param input The {@link ExtractorInput} to read from. - * @param quiet If {@code true}, no exceptions are thrown but {@code false} is returned if - * something goes wrong. - * @return {@code true} if the read was successful. The read fails if the end of the input is - * encountered without reading data. + * @param quiet Whether to return {@code false} rather than throwing an exception if the header + * cannot be populated. + * @return Whether the read was successful. The read fails if the end of the input is encountered + * without reading data. * @throws IOException If reading data fails or the stream is invalid. * @throws InterruptedException If the thread is interrupted. */ From 526cc72e0490760f723d50695d7a46fcf11059e5 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 31 Jul 2019 19:54:12 +0100 Subject: [PATCH 1508/1556] WavExtractor: Skip to data start position if position reset to 0 PiperOrigin-RevId: 260970865 --- .../extractor/wav/WavExtractor.java | 2 ++ .../exoplayer2/extractor/wav/WavHeader.java | 28 +++++++++++++------ .../extractor/wav/WavHeaderReader.java | 2 +- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index 68d252e318..d3114f9b69 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -87,6 +87,8 @@ public final class WavExtractor implements Extractor { if (!wavHeader.hasDataBounds()) { WavHeaderReader.skipToData(input, wavHeader); extractorOutput.seekMap(wavHeader); + } else if (input.getPosition() == 0) { + input.skipFully(wavHeader.getDataStartPosition()); } long dataLimit = wavHeader.getDataLimit(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java index c60117be60..c7858dcd96 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java @@ -37,9 +37,9 @@ import com.google.android.exoplayer2.util.Util; @C.PcmEncoding private final int encoding; - /** Offset to the start of sample data. */ - private long dataStartPosition; - /** Total size in bytes of the sample data. */ + /** Position of the start of the sample data, in bytes. */ + private int dataStartPosition; + /** Total size of the sample data, in bytes. */ private long dataSize; public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, int blockAlignment, @@ -50,6 +50,7 @@ import com.google.android.exoplayer2.util.Util; this.blockAlignment = blockAlignment; this.bitsPerSample = bitsPerSample; this.encoding = encoding; + dataStartPosition = C.POSITION_UNSET; } // Data bounds. @@ -57,22 +58,33 @@ import com.google.android.exoplayer2.util.Util; /** * Sets the data start position and size in bytes of sample data in this WAV. * - * @param dataStartPosition The data start position in bytes. - * @param dataSize The data size in bytes. + * @param dataStartPosition The position of the start of the sample data, in bytes. + * @param dataSize The total size of the sample data, in bytes. */ - public void setDataBounds(long dataStartPosition, long dataSize) { + public void setDataBounds(int dataStartPosition, long dataSize) { this.dataStartPosition = dataStartPosition; this.dataSize = dataSize; } - /** Returns the data limit, or {@link C#POSITION_UNSET} if the data bounds have not been set. */ + /** + * Returns the position of the start of the sample data, in bytes, or {@link C#POSITION_UNSET} if + * the data bounds have not been set. + */ + public int getDataStartPosition() { + return dataStartPosition; + } + + /** + * Returns the limit of the sample data, in bytes, or {@link C#POSITION_UNSET} if the data bounds + * have not been set. + */ public long getDataLimit() { return hasDataBounds() ? (dataStartPosition + dataSize) : C.POSITION_UNSET; } /** Returns whether the data start position and size have been set. */ public boolean hasDataBounds() { - return dataStartPosition != 0 && dataSize != 0; + return dataStartPosition != C.POSITION_UNSET; } // SeekMap implementation. 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 d76d3f37ea..839a9e3d5c 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 @@ -139,7 +139,7 @@ import java.io.IOException; // Skip past the "data" header. input.skipFully(ChunkHeader.SIZE_IN_BYTES); - wavHeader.setDataBounds(input.getPosition(), chunkHeader.size); + wavHeader.setDataBounds((int) input.getPosition(), chunkHeader.size); } private WavHeaderReader() { From 561949a2251b6fc8c96f36771328c0a5e43fe4e2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 1 Aug 2019 09:45:57 +0100 Subject: [PATCH 1509/1556] Remove AnalyticsCollector.Factory. This factory was only needed in the past when we didn't have AnalyticsCollector.setPlayer. Code becomes easier to use without this factory. PiperOrigin-RevId: 261081860 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/ExoPlayerFactory.java | 28 +++++++++---------- .../android/exoplayer2/SimpleExoPlayer.java | 19 +++++++------ .../analytics/AnalyticsCollector.java | 27 ++---------------- .../testutil/ExoPlayerTestRunner.java | 2 +- 5 files changed, 29 insertions(+), 49 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5279a24698..5bb858a2eb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,8 @@ management in playlists. * Improve text selection logic to always prefer the better language matches over other selection parameters. +* Remove `AnalyticsCollector.Factory`. Instances can be created directly and + the `Player` set later using `AnalyticsCollector.setPlayer`. ### 2.10.4 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index 956f22f719..9168f1bd76 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -244,7 +244,7 @@ public final class ExoPlayerFactory { loadControl, drmSessionManager, bandwidthMeter, - new AnalyticsCollector.Factory(), + new AnalyticsCollector(Clock.DEFAULT), Util.getLooper()); } @@ -257,8 +257,8 @@ public final class ExoPlayerFactory { * @param loadControl The {@link LoadControl} that will be used by the instance. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. - * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that - * will collect and forward all player events. + * @param analyticsCollector The {@link AnalyticsCollector} that will collect and forward all + * player events. */ public static SimpleExoPlayer newSimpleInstance( Context context, @@ -266,14 +266,14 @@ public final class ExoPlayerFactory { TrackSelector trackSelector, LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, - AnalyticsCollector.Factory analyticsCollectorFactory) { + AnalyticsCollector analyticsCollector) { return newSimpleInstance( context, renderersFactory, trackSelector, loadControl, drmSessionManager, - analyticsCollectorFactory, + analyticsCollector, Util.getLooper()); } @@ -302,7 +302,7 @@ public final class ExoPlayerFactory { trackSelector, loadControl, drmSessionManager, - new AnalyticsCollector.Factory(), + new AnalyticsCollector(Clock.DEFAULT), looper); } @@ -315,8 +315,8 @@ public final class ExoPlayerFactory { * @param loadControl The {@link LoadControl} that will be used by the instance. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. - * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that - * will collect and forward all player events. + * @param analyticsCollector The {@link AnalyticsCollector} that will collect and forward all + * player events. * @param looper The {@link Looper} which must be used for all calls to the player and which is * used to call listeners on. */ @@ -326,7 +326,7 @@ public final class ExoPlayerFactory { TrackSelector trackSelector, LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, - AnalyticsCollector.Factory analyticsCollectorFactory, + AnalyticsCollector analyticsCollector, Looper looper) { return newSimpleInstance( context, @@ -335,7 +335,7 @@ public final class ExoPlayerFactory { loadControl, drmSessionManager, getDefaultBandwidthMeter(context), - analyticsCollectorFactory, + analyticsCollector, looper); } @@ -348,8 +348,8 @@ public final class ExoPlayerFactory { * @param loadControl The {@link LoadControl} that will be used by the instance. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. - * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that - * will collect and forward all player events. + * @param analyticsCollector The {@link AnalyticsCollector} that will collect and forward all + * player events. * @param looper The {@link Looper} which must be used for all calls to the player and which is * used to call listeners on. */ @@ -360,7 +360,7 @@ public final class ExoPlayerFactory { LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, BandwidthMeter bandwidthMeter, - AnalyticsCollector.Factory analyticsCollectorFactory, + AnalyticsCollector analyticsCollector, Looper looper) { return new SimpleExoPlayer( context, @@ -369,7 +369,7 @@ public final class ExoPlayerFactory { loadControl, drmSessionManager, bandwidthMeter, - analyticsCollectorFactory, + analyticsCollector, looper); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index a782255cb8..8913fbdaba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -144,7 +144,7 @@ public class SimpleExoPlayer extends BasePlayer loadControl, drmSessionManager, bandwidthMeter, - new AnalyticsCollector.Factory(), + new AnalyticsCollector(Clock.DEFAULT), looper); } @@ -156,8 +156,8 @@ public class SimpleExoPlayer extends BasePlayer * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. - * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that - * will collect and forward all player events. + * @param analyticsCollector The {@link AnalyticsCollector} that will collect and forward all + * player events. * @param looper The {@link Looper} which must be used for all calls to the player and which is * used to call listeners on. */ @@ -168,7 +168,7 @@ public class SimpleExoPlayer extends BasePlayer LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, BandwidthMeter bandwidthMeter, - AnalyticsCollector.Factory analyticsCollectorFactory, + AnalyticsCollector analyticsCollector, Looper looper) { this( context, @@ -177,7 +177,7 @@ public class SimpleExoPlayer extends BasePlayer loadControl, drmSessionManager, bandwidthMeter, - analyticsCollectorFactory, + analyticsCollector, Clock.DEFAULT, looper); } @@ -190,8 +190,8 @@ public class SimpleExoPlayer extends BasePlayer * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. - * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that - * will collect and forward all player events. + * @param analyticsCollector The {@link AnalyticsCollector} that will collect and forward all + * player events. * @param clock The {@link Clock} that will be used by the instance. Should always be {@link * Clock#DEFAULT}, unless the player is being used from a test. * @param looper The {@link Looper} which must be used for all calls to the player and which is @@ -204,10 +204,11 @@ public class SimpleExoPlayer extends BasePlayer LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, BandwidthMeter bandwidthMeter, - AnalyticsCollector.Factory analyticsCollectorFactory, + AnalyticsCollector analyticsCollector, Clock clock, Looper looper) { this.bandwidthMeter = bandwidthMeter; + this.analyticsCollector = analyticsCollector; componentListener = new ComponentListener(); videoListeners = new CopyOnWriteArraySet<>(); audioListeners = new CopyOnWriteArraySet<>(); @@ -235,7 +236,7 @@ public class SimpleExoPlayer extends BasePlayer // Build the player and associated objects. player = new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); - analyticsCollector = analyticsCollectorFactory.createAnalyticsCollector(player, clock); + analyticsCollector.setPlayer(player); addListener(analyticsCollector); addListener(componentListener); videoDebugListeners.add(analyticsCollector); 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 091696f8bf..825424ae04 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 @@ -67,23 +67,6 @@ public class AnalyticsCollector VideoListener, AudioListener { - /** Factory for an analytics collector. */ - public static class Factory { - - /** - * Creates an analytics collector for the specified player. - * - * @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(@Nullable Player player, Clock clock) { - return new AnalyticsCollector(player, clock); - } - } - private final CopyOnWriteArraySet listeners; private final Clock clock; private final Window window; @@ -92,17 +75,11 @@ public class AnalyticsCollector private @MonotonicNonNull Player player; /** - * Creates an analytics collector for the specified player. + * Creates an analytics collector. * - * @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(@Nullable Player player, Clock clock) { - if (player != null) { - this.player = player; - } + public AnalyticsCollector(Clock clock) { this.clock = Assertions.checkNotNull(clock); listeners = new CopyOnWriteArraySet<>(); mediaPeriodQueueTracker = new MediaPeriodQueueTracker(); 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 9de7996d3c..7db1987d5b 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 @@ -622,7 +622,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc loadControl, /* drmSessionManager= */ null, bandwidthMeter, - new AnalyticsCollector.Factory(), + new AnalyticsCollector(clock), clock, Looper.myLooper()); } From a2cf427b4b2f6c4922d5f6c108f830e7460cdafd Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 10:34:11 +0100 Subject: [PATCH 1510/1556] Mp3Extractor: Avoid outputting non-zero position seek frame as a sample Checking inputPosition == 0 isn't sufficient because the synchronization at the top of read() may advance the input (i.e. in the case where there's some garbage prior to the seek frame). PiperOrigin-RevId: 261086901 --- .../exoplayer2/extractor/mp3/Mp3Extractor.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index a448934359..ecff963271 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -116,7 +116,7 @@ public final class Mp3Extractor implements Extractor { private Seeker seeker; private long basisTimeUs; private long samplesRead; - private int firstSamplePosition; + private long firstSamplePosition; private int sampleBytesRemaining; public Mp3Extractor() { @@ -214,10 +214,13 @@ public final class Mp3Extractor implements Extractor { /* selectionFlags= */ 0, /* language= */ null, (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); - firstSamplePosition = (int) input.getPosition(); - } else if (input.getPosition() == 0 && firstSamplePosition != 0) { - // Skip past the seek frame. - input.skipFully(firstSamplePosition); + firstSamplePosition = input.getPosition(); + } else if (firstSamplePosition != 0) { + long inputPosition = input.getPosition(); + if (inputPosition < firstSamplePosition) { + // Skip past the seek frame. + input.skipFully((int) (firstSamplePosition - inputPosition)); + } } return readSample(input); } From b2c71e8b3f5162f74e33295009b3dd4a10d51f4b Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Thu, 1 Aug 2019 10:55:50 +0100 Subject: [PATCH 1511/1556] Extract VpxInputBuffer to a common class This class will be shared by both vp9 and av1 extension. PiperOrigin-RevId: 261089225 --- .../exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 5 +++-- .../android/exoplayer2/ext/vp9/VpxDecoder.java | 17 ++++++++--------- .../video/VideoDecoderInputBuffer.java | 13 +++++-------- 3 files changed, 16 insertions(+), 19 deletions(-) rename extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java => library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java (70%) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index 56f5fd2d09..34301742e5 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TimedValueQueue; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.VideoDecoderInputBuffer; import com.google.android.exoplayer2.video.VideoFrameMetadataListener; import com.google.android.exoplayer2.video.VideoRendererEventListener; import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher; @@ -123,7 +124,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { private Format pendingFormat; private Format outputFormat; private VpxDecoder decoder; - private VpxInputBuffer inputBuffer; + private VideoDecoderInputBuffer inputBuffer; private VpxOutputBuffer outputBuffer; @Nullable private DrmSession decoderDrmSession; @Nullable private DrmSession sourceDrmSession; @@ -545,7 +546,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { * * @param buffer The buffer that will be queued. */ - protected void onQueueInputBuffer(VpxInputBuffer buffer) { + protected void onQueueInputBuffer(VideoDecoderInputBuffer buffer) { // Do nothing. } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 0e13e82630..544259ffc0 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -22,13 +22,12 @@ import com.google.android.exoplayer2.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.drm.DecryptionException; import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.video.VideoDecoderInputBuffer; import java.nio.ByteBuffer; -/** - * Vpx decoder. - */ -/* package */ final class VpxDecoder extends - SimpleDecoder { +/** Vpx decoder. */ +/* package */ final class VpxDecoder + extends SimpleDecoder { public static final int OUTPUT_MODE_NONE = -1; public static final int OUTPUT_MODE_YUV = 0; @@ -65,7 +64,7 @@ import java.nio.ByteBuffer; boolean enableRowMultiThreadMode, int threads) throws VpxDecoderException { - super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]); + super(new VideoDecoderInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]); if (!VpxLibrary.isAvailable()) { throw new VpxDecoderException("Failed to load decoder native libraries."); } @@ -96,8 +95,8 @@ import java.nio.ByteBuffer; } @Override - protected VpxInputBuffer createInputBuffer() { - return new VpxInputBuffer(); + protected VideoDecoderInputBuffer createInputBuffer() { + return new VideoDecoderInputBuffer(); } @Override @@ -123,7 +122,7 @@ import java.nio.ByteBuffer; @Override @Nullable protected VpxDecoderException decode( - VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) { + VideoDecoderInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) { ByteBuffer inputData = inputBuffer.data; int inputSize = inputData.limit(); CryptoInfo cryptoInfo = inputBuffer.cryptoInfo; diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java similarity index 70% rename from extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java rename to library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java index fcae9dc6bc..76742a8691 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2019 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. @@ -13,19 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.ext.vp9; +package com.google.android.exoplayer2.video; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.video.ColorInfo; -/** - * Input buffer to a {@link VpxDecoder}. - */ -/* package */ final class VpxInputBuffer extends DecoderInputBuffer { +/** Input buffer to a video decoder. */ +public class VideoDecoderInputBuffer extends DecoderInputBuffer { public ColorInfo colorInfo; - public VpxInputBuffer() { + public VideoDecoderInputBuffer() { super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); } From 1bb0703f2b61f0093d80c65211cd51ba30fd6f03 Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 1 Aug 2019 12:15:59 +0100 Subject: [PATCH 1512/1556] return lg specific mime type as codec supported type for OMX.lge.alac.decoder ISSUE: #5938 PiperOrigin-RevId: 261097045 --- .../exoplayer2/mediacodec/MediaCodecUtil.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index e8fead61ae..46bc448a4a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -370,6 +370,13 @@ public final class MediaCodecUtil { boolean secureDecodersExplicit, String requestedMimeType) { if (isCodecUsableDecoder(info, name, secureDecodersExplicit, requestedMimeType)) { + String[] supportedTypes = info.getSupportedTypes(); + for (String supportedType : supportedTypes) { + if (supportedType.equalsIgnoreCase(requestedMimeType)) { + return supportedType; + } + } + if (requestedMimeType.equals(MimeTypes.VIDEO_DOLBY_VISION)) { // Handle decoders that declare support for DV via MIME types that aren't // video/dolby-vision. @@ -379,13 +386,12 @@ public final class MediaCodecUtil { || "OMX.realtek.video.decoder.tunneled".equals(name)) { return "video/dv_hevc"; } - } - - String[] supportedTypes = info.getSupportedTypes(); - for (String supportedType : supportedTypes) { - if (supportedType.equalsIgnoreCase(requestedMimeType)) { - return supportedType; - } + } else if (requestedMimeType.equals(MimeTypes.AUDIO_ALAC) + && "OMX.lge.alac.decoder".equals(name)) { + return "audio/x-lg-alac"; + } else if (requestedMimeType.equals(MimeTypes.AUDIO_FLAC) + && "OMX.lge.flac.decoder".equals(name)) { + return "audio/x-lg-flac"; } } return null; From cbc1385fd32b55e44768df5416d323936541ff07 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 13:05:07 +0100 Subject: [PATCH 1513/1556] Some no-op cleanup for DefaultOggSeeker PiperOrigin-RevId: 261102008 --- .../extractor/ogg/DefaultOggSeeker.java | 110 ++++++++---------- 1 file changed, 49 insertions(+), 61 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index 9700760c49..a4aa6b8dd5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; @@ -206,39 +207,32 @@ import java.io.IOException; return -(pageHeader.granulePosition + 2); } - private long getEstimatedPosition(long position, long granuleDistance, long offset) { - position += (granuleDistance * (endPosition - startPosition) / totalGranules) - offset; - if (position < startPosition) { - position = startPosition; + /** + * Skips to the position of the start of the page containing the {@code targetGranule} and returns + * the granule of the page previous to the target page. + * + * @param input The {@link ExtractorInput} to read from. + * @param targetGranule The target granule. + * @param currentGranule The current granule or -1 if it's unknown. + * @return The granule of the prior page or the {@code currentGranule} if there isn't a prior + * page. + * @throws ParserException If populating the page header fails. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + */ + @VisibleForTesting + long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule) + throws IOException, InterruptedException { + pageHeader.populate(input, false); + while (pageHeader.granulePosition < targetGranule) { + input.skipFully(pageHeader.headerSize + pageHeader.bodySize); + // Store in a member field to be able to resume after IOExceptions. + currentGranule = pageHeader.granulePosition; + // Peek next header. + pageHeader.populate(input, false); } - if (position >= endPosition) { - position = endPosition - 1; - } - return position; - } - - private class OggSeekMap implements SeekMap { - - @Override - public boolean isSeekable() { - return true; - } - - @Override - public SeekPoints getSeekPoints(long timeUs) { - if (timeUs == 0) { - return new SeekPoints(new SeekPoint(0, startPosition)); - } - long granule = streamReader.convertTimeToGranule(timeUs); - long estimatedPosition = getEstimatedPosition(startPosition, granule, DEFAULT_OFFSET); - return new SeekPoints(new SeekPoint(timeUs, estimatedPosition)); - } - - @Override - public long getDurationUs() { - return streamReader.convertGranuleToTime(totalGranules); - } - + input.resetPeekPosition(); + return currentGranule; } /** @@ -266,8 +260,7 @@ import java.io.IOException; * @throws IOException If peeking/reading from the input fails. * @throws InterruptedException If interrupted while peeking/reading from the input. */ - @VisibleForTesting - boolean skipToNextPage(ExtractorInput input, long limit) + private boolean skipToNextPage(ExtractorInput input, long limit) throws IOException, InterruptedException { limit = Math.min(limit + 3, endPosition); byte[] buffer = new byte[2048]; @@ -317,32 +310,27 @@ import java.io.IOException; return pageHeader.granulePosition; } - /** - * Skips to the position of the start of the page containing the {@code targetGranule} and returns - * the granule of the page previous to the target page. - * - * @param input The {@link ExtractorInput} to read from. - * @param targetGranule The target granule. - * @param currentGranule The current granule or -1 if it's unknown. - * @return The granule of the prior page or the {@code currentGranule} if there isn't a prior - * page. - * @throws ParserException If populating the page header fails. - * @throws IOException If reading from the input fails. - * @throws InterruptedException If interrupted while reading from the input. - */ - @VisibleForTesting - long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule) - throws IOException, InterruptedException { - pageHeader.populate(input, false); - while (pageHeader.granulePosition < targetGranule) { - input.skipFully(pageHeader.headerSize + pageHeader.bodySize); - // Store in a member field to be able to resume after IOExceptions. - currentGranule = pageHeader.granulePosition; - // Peek next header. - pageHeader.populate(input, false); - } - input.resetPeekPosition(); - return currentGranule; - } + private final class OggSeekMap implements SeekMap { + @Override + public boolean isSeekable() { + return true; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + long targetGranule = streamReader.convertTimeToGranule(timeUs); + long estimatedPosition = + startPosition + + (targetGranule * (endPosition - startPosition) / totalGranules) + - DEFAULT_OFFSET; + estimatedPosition = Util.constrainValue(estimatedPosition, startPosition, endPosition - 1); + return new SeekPoints(new SeekPoint(timeUs, estimatedPosition)); + } + + @Override + public long getDurationUs() { + return streamReader.convertGranuleToTime(totalGranules); + } + } } From 95ed5ce65d278d9793f409535a3035e8977cb6e9 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 13:06:45 +0100 Subject: [PATCH 1514/1556] Make OggSeeker.startSeek take a granule rather than a time PiperOrigin-RevId: 261102180 --- .../exoplayer2/extractor/ogg/DefaultOggSeeker.java | 5 ++--- .../android/exoplayer2/extractor/ogg/FlacReader.java | 6 ++---- .../android/exoplayer2/extractor/ogg/OggSeeker.java | 10 ++++------ .../android/exoplayer2/extractor/ogg/StreamReader.java | 9 +++++---- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index a4aa6b8dd5..308547e510 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -120,12 +120,11 @@ import java.io.IOException; } @Override - public long startSeek(long timeUs) { + public void startSeek(long targetGranule) { Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK); - targetGranule = timeUs == 0 ? 0 : streamReader.convertTimeToGranule(timeUs); + this.targetGranule = targetGranule; state = STATE_SEEK; resetSeeking(); - return targetGranule; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java index d4c2bbb485..4efd5c5e11 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java @@ -185,11 +185,9 @@ import java.util.List; } @Override - public long startSeek(long timeUs) { - long granule = convertTimeToGranule(timeUs); - int index = Util.binarySearchFloor(seekPointGranules, granule, true, true); + public void startSeek(long targetGranule) { + int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true); pendingSeekGranule = seekPointGranules[index]; - return granule; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java index aa88e5bf89..e4c3a163e6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java @@ -33,16 +33,14 @@ import java.io.IOException; SeekMap createSeekMap(); /** - * Initializes a seek operation. + * Starts a seek operation. * - * @param timeUs The seek position in microseconds. - * @return The granule position targeted by the seek. + * @param targetGranule The target granule position. */ - long startSeek(long timeUs); + void startSeek(long targetGranule); /** - * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a - * progressive seek. + * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a seek. *

          * If more data is required or if the position of the input needs to be modified then a position * from which data should be provided is returned. Else a negative value is returned. If a seek diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java index e459ad1e58..35a07fcf49 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java @@ -91,7 +91,8 @@ import java.io.IOException; reset(!seekMapSet); } else { if (state != STATE_READ_HEADERS) { - targetGranule = oggSeeker.startSeek(timeUs); + targetGranule = convertTimeToGranule(timeUs); + oggSeeker.startSeek(targetGranule); state = STATE_READ_PAYLOAD; } } @@ -248,13 +249,13 @@ import java.io.IOException; private static final class UnseekableOggSeeker implements OggSeeker { @Override - public long read(ExtractorInput input) throws IOException, InterruptedException { + public long read(ExtractorInput input) { return -1; } @Override - public long startSeek(long timeUs) { - return 0; + public void startSeek(long targetGranule) { + // Do nothing. } @Override From cb8983afd10af000443119fcf3b4f0dcc7637749 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 16:14:12 +0100 Subject: [PATCH 1515/1556] Standardize ALAC initialization data Android considers ALAC initialization data to consider of the magic cookie only, where-as FFmpeg requires a full atom. Standardize around the Android definition, since it makes more sense (the magic cookie being contained within an atom is container specific, where-as the decoder shouldn't care what container the media stream is carried in) Issue: #5938 PiperOrigin-RevId: 261124155 --- .../exoplayer2/ext/ffmpeg/FfmpegDecoder.java | 47 ++++++++++++++----- .../exoplayer2/extractor/mp4/AtomParsers.java | 6 +-- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java index 12c26ca2ec..c78b02aa5b 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java @@ -172,28 +172,49 @@ import java.util.List; private static @Nullable byte[] getExtraData(String mimeType, List initializationData) { switch (mimeType) { case MimeTypes.AUDIO_AAC: - case MimeTypes.AUDIO_ALAC: case MimeTypes.AUDIO_OPUS: return initializationData.get(0); + case MimeTypes.AUDIO_ALAC: + return getAlacExtraData(initializationData); case MimeTypes.AUDIO_VORBIS: - byte[] header0 = initializationData.get(0); - byte[] header1 = initializationData.get(1); - byte[] extraData = new byte[header0.length + header1.length + 6]; - extraData[0] = (byte) (header0.length >> 8); - extraData[1] = (byte) (header0.length & 0xFF); - System.arraycopy(header0, 0, extraData, 2, header0.length); - extraData[header0.length + 2] = 0; - extraData[header0.length + 3] = 0; - extraData[header0.length + 4] = (byte) (header1.length >> 8); - extraData[header0.length + 5] = (byte) (header1.length & 0xFF); - System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length); - return extraData; + return getVorbisExtraData(initializationData); default: // Other codecs do not require extra data. return null; } } + private static byte[] getAlacExtraData(List initializationData) { + // FFmpeg's ALAC decoder expects an ALAC atom, which contains the ALAC "magic cookie", as extra + // data. initializationData[0] contains only the magic cookie, and so we need to package it into + // an ALAC atom. See: + // https://ffmpeg.org/doxygen/0.6/alac_8c.html + // https://github.com/macosforge/alac/blob/master/ALACMagicCookieDescription.txt + byte[] magicCookie = initializationData.get(0); + int alacAtomLength = 12 + magicCookie.length; + ByteBuffer alacAtom = ByteBuffer.allocate(alacAtomLength); + alacAtom.putInt(alacAtomLength); + alacAtom.putInt(0x616c6163); // type=alac + alacAtom.putInt(0); // version=0, flags=0 + alacAtom.put(magicCookie, /* offset= */ 0, magicCookie.length); + return alacAtom.array(); + } + + private static byte[] getVorbisExtraData(List initializationData) { + byte[] header0 = initializationData.get(0); + byte[] header1 = initializationData.get(1); + byte[] extraData = new byte[header0.length + header1.length + 6]; + extraData[0] = (byte) (header0.length >> 8); + extraData[1] = (byte) (header0.length & 0xFF); + System.arraycopy(header0, 0, extraData, 2, header0.length); + extraData[header0.length + 2] = 0; + extraData[header0.length + 3] = 0; + extraData[header0.length + 4] = (byte) (header1.length >> 8); + extraData[header0.length + 5] = (byte) (header1.length & 0xFF); + System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length); + return extraData; + } + private native long ffmpegInitialize( String codecName, @Nullable byte[] extraData, 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 ea45374f86..b3c26246e6 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 @@ -1154,10 +1154,6 @@ import java.util.List; out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); - } else if (childAtomType == Atom.TYPE_alac) { - initializationData = new byte[childAtomSize]; - parent.setPosition(childPosition); - parent.readBytes(initializationData, /* offset= */ 0, childAtomSize); } else if (childAtomType == Atom.TYPE_dOps) { // Build an Opus Identification Header (defined in RFC-7845) by concatenating the Opus Magic // Signature and the body of the dOps atom. @@ -1166,7 +1162,7 @@ import java.util.List; System.arraycopy(opusMagic, 0, initializationData, 0, opusMagic.length); parent.setPosition(childPosition + Atom.HEADER_SIZE); parent.readBytes(initializationData, opusMagic.length, childAtomBodySize); - } else if (childAtomSize == Atom.TYPE_dfLa) { + } else if (childAtomSize == Atom.TYPE_dfLa || childAtomType == Atom.TYPE_alac) { int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE; initializationData = new byte[childAtomBodySize]; parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE); From 79b86de619c5b43fc70680e85518b79db87b1153 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 1 Aug 2019 16:22:17 +0100 Subject: [PATCH 1516/1556] Use per-media source DRM in the Cast demo app PiperOrigin-RevId: 261125341 --- .../exoplayer2/castdemo/MainActivity.java | 16 +++ .../exoplayer2/castdemo/PlayerManager.java | 116 ++++++++++++++++-- demos/cast/src/main/res/values/strings.xml | 4 + 3 files changed, 129 insertions(+), 7 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 244025f90d..d0e40990be 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -35,6 +35,7 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; +import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.cast.MediaItem; @@ -164,8 +165,23 @@ public class MainActivity extends AppCompatActivity } } + @Override + public void onUnsupportedTrack(int trackType) { + if (trackType == C.TRACK_TYPE_AUDIO) { + showToast(R.string.error_unsupported_audio); + } else if (trackType == C.TRACK_TYPE_VIDEO) { + showToast(R.string.error_unsupported_video); + } else { + // Do nothing. + } + } + // Internal methods. + private void showToast(int messageId) { + Toast.makeText(getApplicationContext(), messageId, Toast.LENGTH_LONG).show(); + } + private View buildSampleListView() { View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null); ListView sampleList = dialogList.findViewById(R.id.sample_list); 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 44d9a60ff2..8b75eb0c74 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 @@ -28,6 +28,12 @@ import com.google.android.exoplayer2.Player.TimelineChangeReason; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.drm.FrameworkMediaDrm; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.ext.cast.CastPlayer; import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter; import com.google.android.exoplayer2.ext.cast.MediaItem; @@ -36,15 +42,21 @@ import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.Map; /** Manages players and an internal media queue for the demo app. */ /* package */ class PlayerManager implements EventListener, SessionAvailabilityListener { @@ -54,6 +66,13 @@ import java.util.ArrayList; /** Called when the currently played item of the media queue changes. */ void onQueuePositionChanged(int previousIndex, int newIndex); + + /** + * Called when a track of type {@code trackType} is not supported by the player. + * + * @param trackType One of the {@link C}{@code .TRACK_TYPE_*} constants. + */ + void onUnsupportedTrack(int trackType); } private static final String USER_AGENT = "ExoCastDemoPlayer"; @@ -62,13 +81,16 @@ import java.util.ArrayList; private final PlayerView localPlayerView; private final PlayerControlView castControlView; + private final DefaultTrackSelector trackSelector; private final SimpleExoPlayer exoPlayer; private final CastPlayer castPlayer; private final ArrayList mediaQueue; private final Listener listener; private final ConcatenatingMediaSource concatenatingMediaSource; private final MediaItemConverter mediaItemConverter; + private final IdentityHashMap mediaDrms; + private TrackGroupArray lastSeenTrackGroupArray; private int currentItemIndex; private Player currentPlayer; @@ -94,8 +116,10 @@ import java.util.ArrayList; currentItemIndex = C.INDEX_UNSET; concatenatingMediaSource = new ConcatenatingMediaSource(); mediaItemConverter = new DefaultMediaItemConverter(); + mediaDrms = new IdentityHashMap<>(); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context); + trackSelector = new DefaultTrackSelector(context); + exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector); exoPlayer.addListener(this); localPlayerView.setPlayer(exoPlayer); @@ -162,7 +186,8 @@ import java.util.ArrayList; if (itemIndex == -1) { return false; } - concatenatingMediaSource.removeMediaSource(itemIndex); + MediaSource removedMediaSource = concatenatingMediaSource.removeMediaSource(itemIndex); + releaseMediaDrmOfMediaSource(removedMediaSource); if (currentPlayer == castPlayer) { if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { Timeline castTimeline = castPlayer.getCurrentTimeline(); @@ -238,6 +263,9 @@ import java.util.ArrayList; currentItemIndex = C.INDEX_UNSET; mediaQueue.clear(); concatenatingMediaSource.clear(); + for (FrameworkMediaDrm mediaDrm : mediaDrms.values()) { + mediaDrm.release(); + } castPlayer.setSessionAvailabilityListener(null); castPlayer.release(); localPlayerView.setPlayer(null); @@ -261,6 +289,25 @@ import java.util.ArrayList; updateCurrentItemIndex(); } + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + if (currentPlayer == exoPlayer && trackGroups != lastSeenTrackGroupArray) { + MappingTrackSelector.MappedTrackInfo mappedTrackInfo = + trackSelector.getCurrentMappedTrackInfo(); + if (mappedTrackInfo != null) { + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) + == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + listener.onUnsupportedTrack(C.TRACK_TYPE_VIDEO); + } + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) + == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + listener.onUnsupportedTrack(C.TRACK_TYPE_AUDIO); + } + } + lastSeenTrackGroupArray = trackGroups; + } + } + // CastPlayer.SessionAvailabilityListener implementation. @Override @@ -360,23 +407,78 @@ import java.util.ArrayList; } } - private static MediaSource buildMediaSource(MediaItem item) { + private MediaSource buildMediaSource(MediaItem item) { Uri uri = item.uri; String mimeType = item.mimeType; if (mimeType == null) { throw new IllegalArgumentException("mimeType is required"); } + + FrameworkMediaDrm mediaDrm = null; + DrmSessionManager drmSessionManager = + DrmSessionManager.getDummyDrmSessionManager(); + MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; + if (drmConfiguration != null) { + String licenseServerUrl = + drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : ""; + HttpMediaDrmCallback drmCallback = + new HttpMediaDrmCallback(licenseServerUrl, DATA_SOURCE_FACTORY); + for (Map.Entry requestHeader : drmConfiguration.requestHeaders.entrySet()) { + drmCallback.setKeyRequestProperty(requestHeader.getKey(), requestHeader.getValue()); + } + try { + mediaDrm = FrameworkMediaDrm.newInstance(drmConfiguration.uuid); + drmSessionManager = + new DefaultDrmSessionManager<>( + drmConfiguration.uuid, + mediaDrm, + drmCallback, + /* optionalKeyRequestParameters= */ null, + /* multiSession= */ true); + } catch (UnsupportedDrmException e) { + // Do nothing. The track selector will avoid selecting the DRM protected tracks. + } + } + + MediaSource createdMediaSource; switch (mimeType) { case DemoUtil.MIME_TYPE_SS: - return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + createdMediaSource = + new SsMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; case DemoUtil.MIME_TYPE_DASH: - return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + createdMediaSource = + new DashMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; case DemoUtil.MIME_TYPE_HLS: - return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + createdMediaSource = + new HlsMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; case DemoUtil.MIME_TYPE_VIDEO_MP4: - return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + createdMediaSource = + new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; default: throw new IllegalArgumentException("mimeType is unsupported: " + mimeType); } + if (mediaDrm != null) { + mediaDrms.put(createdMediaSource, mediaDrm); + } + return createdMediaSource; + } + + private void releaseMediaDrmOfMediaSource(MediaSource mediaSource) { + FrameworkMediaDrm mediaDrmToRelease = mediaDrms.remove(mediaSource); + if (mediaDrmToRelease != null) { + mediaDrmToRelease.release(); + } } } diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml index 2f0acd4808..69f0691630 100644 --- a/demos/cast/src/main/res/values/strings.xml +++ b/demos/cast/src/main/res/values/strings.xml @@ -24,4 +24,8 @@ Failed to get Cast context. Try updating Google Play Services and restart the app. + Media includes video tracks, but none are playable by this device + + Media includes audio tracks, but none are playable by this device + From 4c40878b6b8d6662fd12e4068ed885e7332e2701 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 16:32:29 +0100 Subject: [PATCH 1517/1556] Shorten data length if it exceeds length of input Issue: #6241 PiperOrigin-RevId: 261126968 --- RELEASENOTES.md | 2 ++ .../extractor/wav/WavExtractor.java | 6 ++-- .../exoplayer2/extractor/wav/WavHeader.java | 36 +++++++++++-------- .../extractor/wav/WavHeaderReader.java | 13 +++++-- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c37556c98c..3c1748ea9d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,6 +27,8 @@ over other selection parameters. * Remove `AnalyticsCollector.Factory`. Instances can be created directly and the `Player` set later using `AnalyticsCollector.setPlayer`. +* Calculate correct duration for clipped WAV streams + ([#6241](https://github.com/google/ExoPlayer/issues/6241)). ### 2.10.4 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index d3114f9b69..91097c9e5b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -91,10 +91,10 @@ public final class WavExtractor implements Extractor { input.skipFully(wavHeader.getDataStartPosition()); } - long dataLimit = wavHeader.getDataLimit(); - Assertions.checkState(dataLimit != C.POSITION_UNSET); + long dataEndPosition = wavHeader.getDataEndPosition(); + Assertions.checkState(dataEndPosition != C.POSITION_UNSET); - long bytesLeft = dataLimit - input.getPosition(); + long bytesLeft = dataEndPosition - input.getPosition(); if (bytesLeft <= 0) { return Extractor.RESULT_END_OF_INPUT; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java index c7858dcd96..6e3c5988a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java @@ -33,17 +33,21 @@ import com.google.android.exoplayer2.util.Util; private final int blockAlignment; /** Bits per sample for the audio data. */ private final int bitsPerSample; - /** The PCM encoding */ - @C.PcmEncoding - private final int encoding; + /** The PCM encoding. */ + @C.PcmEncoding private final int encoding; /** Position of the start of the sample data, in bytes. */ private int dataStartPosition; - /** Total size of the sample data, in bytes. */ - private long dataSize; + /** Position of the end of the sample data (exclusive), in bytes. */ + private long dataEndPosition; - public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, int blockAlignment, - int bitsPerSample, @C.PcmEncoding int encoding) { + public WavHeader( + int numChannels, + int sampleRateHz, + int averageBytesPerSecond, + int blockAlignment, + int bitsPerSample, + @C.PcmEncoding int encoding) { this.numChannels = numChannels; this.sampleRateHz = sampleRateHz; this.averageBytesPerSecond = averageBytesPerSecond; @@ -51,6 +55,7 @@ import com.google.android.exoplayer2.util.Util; this.bitsPerSample = bitsPerSample; this.encoding = encoding; dataStartPosition = C.POSITION_UNSET; + dataEndPosition = C.POSITION_UNSET; } // Data bounds. @@ -59,11 +64,11 @@ import com.google.android.exoplayer2.util.Util; * Sets the data start position and size in bytes of sample data in this WAV. * * @param dataStartPosition The position of the start of the sample data, in bytes. - * @param dataSize The total size of the sample data, in bytes. + * @param dataEndPosition The position of the end of the sample data (exclusive), in bytes. */ - public void setDataBounds(int dataStartPosition, long dataSize) { + public void setDataBounds(int dataStartPosition, long dataEndPosition) { this.dataStartPosition = dataStartPosition; - this.dataSize = dataSize; + this.dataEndPosition = dataEndPosition; } /** @@ -75,11 +80,11 @@ import com.google.android.exoplayer2.util.Util; } /** - * Returns the limit of the sample data, in bytes, or {@link C#POSITION_UNSET} if the data bounds - * have not been set. + * Returns the position of the end of the sample data (exclusive), in bytes, or {@link + * C#POSITION_UNSET} if the data bounds have not been set. */ - public long getDataLimit() { - return hasDataBounds() ? (dataStartPosition + dataSize) : C.POSITION_UNSET; + public long getDataEndPosition() { + return dataEndPosition; } /** Returns whether the data start position and size have been set. */ @@ -96,12 +101,13 @@ import com.google.android.exoplayer2.util.Util; @Override public long getDurationUs() { - long numFrames = dataSize / blockAlignment; + long numFrames = (dataEndPosition - dataStartPosition) / blockAlignment; return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz; } @Override public SeekPoints getSeekPoints(long timeUs) { + long dataSize = dataEndPosition - dataStartPosition; long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; // Constrain to nearest preceding frame offset. positionOffset = (positionOffset / blockAlignment) * blockAlignment; 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 839a9e3d5c..bbcb75aa2d 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 @@ -91,8 +91,8 @@ import java.io.IOException; // If present, skip extensionSize, validBitsPerSample, channelMask, subFormatGuid, ... input.advancePeekPosition((int) chunkHeader.size - 16); - return new WavHeader(numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, - bitsPerSample, encoding); + return new WavHeader( + numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, bitsPerSample, encoding); } /** @@ -139,7 +139,14 @@ import java.io.IOException; // Skip past the "data" header. input.skipFully(ChunkHeader.SIZE_IN_BYTES); - wavHeader.setDataBounds((int) input.getPosition(), chunkHeader.size); + int dataStartPosition = (int) input.getPosition(); + long dataEndPosition = dataStartPosition + chunkHeader.size; + long inputLength = input.getLength(); + if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) { + Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength); + dataEndPosition = inputLength; + } + wavHeader.setDataBounds(dataStartPosition, dataEndPosition); } private WavHeaderReader() { From 42d3ca273ba94c85169f8a6697a8e9282a4a5737 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 18:39:20 +0100 Subject: [PATCH 1518/1556] Propagate non-standard MIME type aliases Issue: #5938 PiperOrigin-RevId: 261150349 --- RELEASENOTES.md | 2 + .../audio/MediaCodecAudioRenderer.java | 2 +- .../exoplayer2/mediacodec/MediaCodecInfo.java | 41 +++--- .../exoplayer2/mediacodec/MediaCodecUtil.java | 118 ++++++++++-------- .../video/MediaCodecVideoRenderer.java | 6 +- 5 files changed, 92 insertions(+), 77 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3c1748ea9d..9dd79d8c42 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,8 @@ the `Player` set later using `AnalyticsCollector.setPlayer`. * Calculate correct duration for clipped WAV streams ([#6241](https://github.com/google/ExoPlayer/issues/6241)). +* Fix Flac and ALAC playback on some LG devices + ([#5938](https://github.com/google/ExoPlayer/issues/5938)). ### 2.10.4 ### 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 b965f4ef68..6a29f316e1 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 @@ -392,7 +392,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); codecNeedsEosBufferTimestampWorkaround = codecNeedsEosBufferTimestampWorkaround(codecInfo.name); passthroughEnabled = codecInfo.passthrough; - String codecMimeType = passthroughEnabled ? MimeTypes.AUDIO_RAW : codecInfo.mimeType; + String codecMimeType = passthroughEnabled ? MimeTypes.AUDIO_RAW : codecInfo.codecMimeType; MediaFormat mediaFormat = getMediaFormat(format, codecMimeType, codecMaxInputSize, codecOperatingRate); codec.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index acaf798b41..d07def1894 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -53,6 +53,13 @@ public final class MediaCodecInfo { /** The MIME type handled by the codec, or {@code null} if this is a passthrough codec. */ @Nullable public final String mimeType; + /** + * The MIME type that the codec uses for media of type {@link #mimeType}, or {@code null} if this + * is a passthrough codec. Equal to {@link #mimeType} unless the codec is known to use a + * non-standard MIME type alias. + */ + @Nullable public final String codecMimeType; + /** * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if not * known. @@ -98,6 +105,7 @@ public final class MediaCodecInfo { return new MediaCodecInfo( name, /* mimeType= */ null, + /* codecMimeType= */ null, /* capabilities= */ null, /* passthrough= */ true, /* forceDisableAdaptive= */ false, @@ -109,26 +117,8 @@ public final class MediaCodecInfo { * * @param name The name of the {@link MediaCodec}. * @param mimeType A mime type supported by the {@link MediaCodec}. - * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type, or - * {@code null} if not known. - * @return The created instance. - */ - public static MediaCodecInfo newInstance( - String name, String mimeType, @Nullable CodecCapabilities capabilities) { - return new MediaCodecInfo( - name, - mimeType, - capabilities, - /* passthrough= */ false, - /* forceDisableAdaptive= */ false, - /* forceSecure= */ false); - } - - /** - * Creates an instance. - * - * @param name The name of the {@link MediaCodec}. - * @param mimeType A mime type supported by the {@link MediaCodec}. + * @param codecMimeType The MIME type that the codec uses for media of type {@code #mimeType}. + * Equal to {@code mimeType} unless the codec is known to use a non-standard MIME type alias. * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type, or * {@code null} if not known. * @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}. @@ -138,22 +128,31 @@ public final class MediaCodecInfo { public static MediaCodecInfo newInstance( String name, String mimeType, + String codecMimeType, @Nullable CodecCapabilities capabilities, boolean forceDisableAdaptive, boolean forceSecure) { return new MediaCodecInfo( - name, mimeType, capabilities, /* passthrough= */ false, forceDisableAdaptive, forceSecure); + name, + mimeType, + codecMimeType, + capabilities, + /* passthrough= */ false, + forceDisableAdaptive, + forceSecure); } private MediaCodecInfo( String name, @Nullable String mimeType, + @Nullable String codecMimeType, @Nullable CodecCapabilities capabilities, boolean passthrough, boolean forceDisableAdaptive, boolean forceSecure) { this.name = Assertions.checkNotNull(name); this.mimeType = mimeType; + this.codecMimeType = codecMimeType; this.capabilities = capabilities; this.passthrough = passthrough; adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 46bc448a4a..cd4c4863ff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -171,12 +171,12 @@ public final class MediaCodecUtil { Util.SDK_INT >= 21 ? new MediaCodecListCompatV21(secure, tunneling) : new MediaCodecListCompatV16(); - ArrayList decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); + ArrayList decoderInfos = getDecoderInfosInternal(key, mediaCodecList); if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) { // Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the // legacy path. We also try this path on API levels 22 and 23 as a defensive measure. mediaCodecList = new MediaCodecListCompatV16(); - decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); + decoderInfos = getDecoderInfosInternal(key, mediaCodecList); if (!decoderInfos.isEmpty()) { Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType + ". Assuming: " + decoderInfos.get(0).name); @@ -268,18 +268,16 @@ public final class MediaCodecUtil { // Internal methods. /** - * Returns {@link MediaCodecInfo}s for the given codec {@code key} in the order given by + * Returns {@link MediaCodecInfo}s for the given codec {@link CodecKey} in the order given by * {@code mediaCodecList}. * * @param key The codec key. * @param mediaCodecList The codec list. - * @param requestedMimeType The originally requested MIME type, which may differ from the codec - * key MIME type if the codec key is being considered as a fallback. * @return The codec information for usable codecs matching the specified key. * @throws DecoderQueryException If there was an error querying the available decoders. */ - private static ArrayList getDecoderInfosInternal(CodecKey key, - MediaCodecListCompat mediaCodecList, String requestedMimeType) throws DecoderQueryException { + private static ArrayList getDecoderInfosInternal( + CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException { try { ArrayList decoderInfos = new ArrayList<>(); String mimeType = key.mimeType; @@ -289,28 +287,27 @@ public final class MediaCodecUtil { for (int i = 0; i < numberOfCodecs; i++) { android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i); String name = codecInfo.getName(); - String supportedType = - getCodecSupportedType(codecInfo, name, secureDecodersExplicit, requestedMimeType); - if (supportedType == null) { + String codecMimeType = getCodecMimeType(codecInfo, name, secureDecodersExplicit, mimeType); + if (codecMimeType == null) { continue; } try { - CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(supportedType); + CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(codecMimeType); boolean tunnelingSupported = mediaCodecList.isFeatureSupported( - CodecCapabilities.FEATURE_TunneledPlayback, supportedType, capabilities); + CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities); boolean tunnelingRequired = mediaCodecList.isFeatureRequired( - CodecCapabilities.FEATURE_TunneledPlayback, supportedType, capabilities); + CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities); if ((!key.tunneling && tunnelingRequired) || (key.tunneling && !tunnelingSupported)) { continue; } boolean secureSupported = mediaCodecList.isFeatureSupported( - CodecCapabilities.FEATURE_SecurePlayback, supportedType, capabilities); + CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities); boolean secureRequired = mediaCodecList.isFeatureRequired( - CodecCapabilities.FEATURE_SecurePlayback, supportedType, capabilities); + CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities); if ((!key.secure && secureRequired) || (key.secure && !secureSupported)) { continue; } @@ -319,12 +316,18 @@ public final class MediaCodecUtil { || (!secureDecodersExplicit && !key.secure)) { decoderInfos.add( MediaCodecInfo.newInstance( - name, mimeType, capabilities, forceDisableAdaptive, /* forceSecure= */ false)); + name, + mimeType, + codecMimeType, + capabilities, + forceDisableAdaptive, + /* forceSecure= */ false)); } else if (!secureDecodersExplicit && secureSupported) { decoderInfos.add( MediaCodecInfo.newInstance( name + ".secure", mimeType, + codecMimeType, capabilities, forceDisableAdaptive, /* forceSecure= */ true)); @@ -338,7 +341,7 @@ public final class MediaCodecUtil { } else { // Rethrow error querying primary codec capabilities, or secondary codec // capabilities if API level is greater than 23. - Log.e(TAG, "Failed to query codec " + name + " (" + supportedType + ")"); + Log.e(TAG, "Failed to query codec " + name + " (" + codecMimeType + ")"); throw e; } } @@ -352,48 +355,49 @@ public final class MediaCodecUtil { } /** - * Returns the codec's supported type for decoding {@code requestedMimeType} on the current - * device, or {@code null} if the codec can't be used. + * Returns the codec's supported MIME type for media of type {@code mimeType}, or {@code null} if + * the codec can't be used. * * @param info The codec information. * @param name The name of the codec * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. - * @param requestedMimeType The originally requested MIME type, which may differ from the codec - * key MIME type if the codec key is being considered as a fallback. - * @return The codec's supported type for decoding {@code requestedMimeType}, or {@code null} if - * the codec can't be used. + * @param mimeType The MIME type. + * @return The codec's supported MIME type for media of type {@code mimeType}, or {@code null} if + * the codec can't be used. If non-null, the returned type will be equal to {@code mimeType} + * except in cases where the codec is known to use a non-standard MIME type alias. */ @Nullable - private static String getCodecSupportedType( + private static String getCodecMimeType( android.media.MediaCodecInfo info, String name, boolean secureDecodersExplicit, - String requestedMimeType) { - if (isCodecUsableDecoder(info, name, secureDecodersExplicit, requestedMimeType)) { - String[] supportedTypes = info.getSupportedTypes(); - for (String supportedType : supportedTypes) { - if (supportedType.equalsIgnoreCase(requestedMimeType)) { - return supportedType; - } - } + String mimeType) { + if (!isCodecUsableDecoder(info, name, secureDecodersExplicit, mimeType)) { + return null; + } - if (requestedMimeType.equals(MimeTypes.VIDEO_DOLBY_VISION)) { - // Handle decoders that declare support for DV via MIME types that aren't - // video/dolby-vision. - if ("OMX.MS.HEVCDV.Decoder".equals(name)) { - return "video/hevcdv"; - } else if ("OMX.RTK.video.decoder".equals(name) - || "OMX.realtek.video.decoder.tunneled".equals(name)) { - return "video/dv_hevc"; - } - } else if (requestedMimeType.equals(MimeTypes.AUDIO_ALAC) - && "OMX.lge.alac.decoder".equals(name)) { - return "audio/x-lg-alac"; - } else if (requestedMimeType.equals(MimeTypes.AUDIO_FLAC) - && "OMX.lge.flac.decoder".equals(name)) { - return "audio/x-lg-flac"; + String[] supportedTypes = info.getSupportedTypes(); + for (String supportedType : supportedTypes) { + if (supportedType.equalsIgnoreCase(mimeType)) { + return supportedType; } } + + if (mimeType.equals(MimeTypes.VIDEO_DOLBY_VISION)) { + // Handle decoders that declare support for DV via MIME types that aren't + // video/dolby-vision. + if ("OMX.MS.HEVCDV.Decoder".equals(name)) { + return "video/hevcdv"; + } else if ("OMX.RTK.video.decoder".equals(name) + || "OMX.realtek.video.decoder.tunneled".equals(name)) { + return "video/dv_hevc"; + } + } else if (mimeType.equals(MimeTypes.AUDIO_ALAC) && "OMX.lge.alac.decoder".equals(name)) { + return "audio/x-lg-alac"; + } else if (mimeType.equals(MimeTypes.AUDIO_FLAC) && "OMX.lge.flac.decoder".equals(name)) { + return "audio/x-lg-flac"; + } + return null; } @@ -403,12 +407,14 @@ public final class MediaCodecUtil { * @param info The codec information. * @param name The name of the codec * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. - * @param requestedMimeType The originally requested MIME type, which may differ from the codec - * key MIME type if the codec key is being considered as a fallback. + * @param mimeType The MIME type. * @return Whether the specified codec is usable for decoding on the current device. */ - private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, String name, - boolean secureDecodersExplicit, String requestedMimeType) { + private static boolean isCodecUsableDecoder( + android.media.MediaCodecInfo info, + String name, + boolean secureDecodersExplicit, + String mimeType) { if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) { return false; } @@ -497,8 +503,7 @@ public final class MediaCodecUtil { } // MTK E-AC3 decoder doesn't support decoding JOC streams in 2-D. See [Internal: b/69400041]. - if (MimeTypes.AUDIO_E_AC3_JOC.equals(requestedMimeType) - && "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) { + if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType) && "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) { return false; } @@ -522,7 +527,12 @@ public final class MediaCodecUtil { // name. See Issue #5782. decoderInfos.add( MediaCodecInfo.newInstance( - "OMX.google.raw.decoder", MimeTypes.AUDIO_RAW, /* capabilities= */ null)); + /* name= */ "OMX.google.raw.decoder", + /* mimeType= */ MimeTypes.AUDIO_RAW, + /* codecMimeType= */ MimeTypes.AUDIO_RAW, + /* capabilities= */ null, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false)); } // Work around inconsistent raw audio decoding behavior across different devices. sortByScore( 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 24524d057d..2ab7e61378 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 @@ -603,10 +603,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { Format format, MediaCrypto crypto, float codecOperatingRate) { + String codecMimeType = codecInfo.codecMimeType; codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); MediaFormat mediaFormat = getMediaFormat( format, + codecMimeType, codecMaxValues, codecOperatingRate, deviceNeedsNoPostProcessWorkaround, @@ -1164,6 +1166,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * Returns the framework {@link MediaFormat} that should be used to configure the decoder. * * @param format The format of media. + * @param codecMimeType The MIME type handled by the codec. * @param codecMaxValues Codec max values that should be used when configuring the decoder. * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if * no codec operating rate should be set. @@ -1176,13 +1179,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @SuppressLint("InlinedApi") protected MediaFormat getMediaFormat( Format format, + String codecMimeType, CodecMaxValues codecMaxValues, float codecOperatingRate, boolean deviceNeedsNoPostProcessWorkaround, int tunnelingAudioSessionId) { MediaFormat mediaFormat = new MediaFormat(); // Set format parameters that should always be set. - mediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType); + mediaFormat.setString(MediaFormat.KEY_MIME, codecMimeType); mediaFormat.setInteger(MediaFormat.KEY_WIDTH, format.width); mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, format.height); MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); From 5eab519925c0d125074f770eae722121700ef2c5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 1 Aug 2019 19:36:18 +0100 Subject: [PATCH 1519/1556] Revert to using header bitrate for CBR MP3s A previous change switched to calculation of the bitrate based on the first MPEG audio header in the stream. This had the effect of fixing seeking to be consistent with playing from the start for streams where every frame has the same padding value, but broke streams where the encoder (correctly) modifies the padding value to match the declared bitrate in the header. Issue: #6238 PiperOrigin-RevId: 261163904 --- RELEASENOTES.md | 2 ++ .../google/android/exoplayer2/extractor/MpegAudioHeader.java | 4 ---- library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump | 2 +- library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump | 2 +- library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump | 2 +- library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump | 2 +- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9dd79d8c42..89acdfb9a4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -31,6 +31,8 @@ ([#6241](https://github.com/google/ExoPlayer/issues/6241)). * Fix Flac and ALAC playback on some LG devices ([#5938](https://github.com/google/ExoPlayer/issues/5938)). +* MP3: use CBR header bitrate, not calculated bitrate. This reverts a change + from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)). ### 2.10.4 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java index 87bb992082..e454bd51c8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java @@ -186,10 +186,6 @@ public final class MpegAudioHeader { } } - // Calculate the bitrate in the same way Mp3Extractor calculates sample timestamps so that - // seeking to a given timestamp and playing from the start up to that timestamp give the same - // results for CBR streams. See also [internal: b/120390268]. - bitrate = 8 * frameSize * sampleRate / samplesPerFrame; String mimeType = MIME_TYPE_BY_LAYER[3 - layer]; int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2; header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame); diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump index d4df3ffeba..96b0cd259c 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26122 + duration = 26125 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump index d4df3ffeba..96b0cd259c 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26122 + duration = 26125 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump index d4df3ffeba..96b0cd259c 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26122 + duration = 26125 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump index d4df3ffeba..96b0cd259c 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26122 + duration = 26125 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: From 0887ab059c1e78257ebc45ec86c9807c05094698 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 2 Aug 2019 09:28:17 +0100 Subject: [PATCH 1520/1556] Remove nullablity of track groups and selections in MediaPeriodHolder. These values can easily default to the empty track group and the empty selection. As a result we can remove restrictions about not calling holder.getTrackGroups before the period finished preparation. PiperOrigin-RevId: 261280927 --- .../exoplayer2/ExoPlayerImplInternal.java | 20 ++++---- .../android/exoplayer2/MediaPeriodHolder.java | 48 ++++++++----------- .../android/exoplayer2/MediaPeriodQueue.java | 9 +++- .../trackselection/TrackSelectorResult.java | 4 +- .../exoplayer2/MediaPeriodQueueTest.java | 10 +++- 5 files changed, 49 insertions(+), 42 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 488d002ab2..4fe8da92c2 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 @@ -1108,7 +1108,7 @@ import java.util.concurrent.atomic.AtomicBoolean; return; } newTrackSelectorResult = periodHolder.selectTracks(playbackSpeed, playbackInfo.timeline); - if (newTrackSelectorResult != null) { + if (!newTrackSelectorResult.isEquivalent(periodHolder.getTrackSelectorResult())) { // Selected tracks have changed for this period. break; } @@ -1197,13 +1197,10 @@ import java.util.concurrent.atomic.AtomicBoolean; private void notifyTrackSelectionDiscontinuity() { MediaPeriodHolder periodHolder = queue.getFrontPeriod(); while (periodHolder != null) { - TrackSelectorResult trackSelectorResult = periodHolder.getTrackSelectorResult(); - if (trackSelectorResult != null) { - TrackSelection[] trackSelections = trackSelectorResult.selections.getAll(); - for (TrackSelection trackSelection : trackSelections) { - if (trackSelection != null) { - trackSelection.onDiscontinuity(); - } + TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll(); + for (TrackSelection trackSelection : trackSelections) { + if (trackSelection != null) { + trackSelection.onDiscontinuity(); } } periodHolder = periodHolder.getNext(); @@ -1506,7 +1503,12 @@ import java.util.concurrent.atomic.AtomicBoolean; } else { MediaPeriod mediaPeriod = queue.enqueueNextMediaPeriod( - rendererCapabilities, trackSelector, loadControl.getAllocator(), mediaSource, info); + rendererCapabilities, + trackSelector, + loadControl.getAllocator(), + mediaSource, + info, + emptyTrackSelectorResult); mediaPeriod.prepare(this, info.startPositionUs); setIsLoading(true); handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java index a21afc4b51..850d2b7d10 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java @@ -59,8 +59,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private final MediaSource mediaSource; @Nullable private MediaPeriodHolder next; - @Nullable private TrackGroupArray trackGroups; - @Nullable private TrackSelectorResult trackSelectorResult; + private TrackGroupArray trackGroups; + private TrackSelectorResult trackSelectorResult; private long rendererPositionOffsetUs; /** @@ -72,6 +72,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * @param allocator The allocator. * @param mediaSource The media source that produced the media period. * @param info Information used to identify this media period in its timeline period. + * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each + * renderer. */ public MediaPeriodHolder( RendererCapabilities[] rendererCapabilities, @@ -79,13 +81,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; TrackSelector trackSelector, Allocator allocator, MediaSource mediaSource, - MediaPeriodInfo info) { + MediaPeriodInfo info, + TrackSelectorResult emptyTrackSelectorResult) { this.rendererCapabilities = rendererCapabilities; this.rendererPositionOffsetUs = rendererPositionOffsetUs; this.trackSelector = trackSelector; this.mediaSource = mediaSource; this.uid = info.id.periodUid; this.info = info; + this.trackGroups = TrackGroupArray.EMPTY; + this.trackSelectorResult = emptyTrackSelectorResult; sampleStreams = new SampleStream[rendererCapabilities.length]; mayRetainStreamFlags = new boolean[rendererCapabilities.length]; mediaPeriod = @@ -167,8 +172,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; public void handlePrepared(float playbackSpeed, Timeline timeline) throws ExoPlaybackException { prepared = true; trackGroups = mediaPeriod.getTrackGroups(); - TrackSelectorResult selectorResult = - Assertions.checkNotNull(selectTracks(playbackSpeed, timeline)); + TrackSelectorResult selectorResult = selectTracks(playbackSpeed, timeline); long newStartPositionUs = applyTrackSelection( selectorResult, info.startPositionUs, /* forceRecreateStreams= */ false); @@ -202,22 +206,20 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } /** - * Selects tracks for the period and returns the new result if the selection changed. Must only be - * called if {@link #prepared} is {@code true}. + * Selects tracks for the period. Must only be called if {@link #prepared} is {@code true}. + * + *

          The new track selection needs to be applied with {@link + * #applyTrackSelection(TrackSelectorResult, long, boolean)} before taking effect. * * @param playbackSpeed The current playback speed. * @param timeline The current {@link Timeline}. - * @return The {@link TrackSelectorResult} if the result changed. Or null if nothing changed. + * @return The {@link TrackSelectorResult}. * @throws ExoPlaybackException If an error occurs during track selection. */ - @Nullable public TrackSelectorResult selectTracks(float playbackSpeed, Timeline timeline) throws ExoPlaybackException { TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities, getTrackGroups(), info.id, timeline); - if (selectorResult.isEquivalent(trackSelectorResult)) { - return null; - } for (TrackSelection trackSelection : selectorResult.selections.getAll()) { if (trackSelection != null) { trackSelection.onPlaybackSpeed(playbackSpeed); @@ -303,7 +305,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Releases the media period. No other method should be called after the release. */ public void release() { disableTrackSelectionsInResult(); - trackSelectorResult = null; releaseMediaPeriod(info.endPositionUs, mediaSource, mediaPeriod); } @@ -331,25 +332,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return next; } - /** - * Returns the {@link TrackGroupArray} exposed by this media period. Must only be called if {@link - * #prepared} is {@code true}. - */ + /** Returns the {@link TrackGroupArray} exposed by this media period. */ public TrackGroupArray getTrackGroups() { - return Assertions.checkNotNull(trackGroups); + return trackGroups; } - /** - * Returns the {@link TrackSelectorResult} which is currently applied. Must only be called if - * {@link #prepared} is {@code true}. - */ + /** Returns the {@link TrackSelectorResult} which is currently applied. */ public TrackSelectorResult getTrackSelectorResult() { - return Assertions.checkNotNull(trackSelectorResult); + return trackSelectorResult; } private void enableTrackSelectionsInResult() { - TrackSelectorResult trackSelectorResult = this.trackSelectorResult; - if (!isLoadingMediaPeriod() || trackSelectorResult == null) { + if (!isLoadingMediaPeriod()) { return; } for (int i = 0; i < trackSelectorResult.length; i++) { @@ -362,8 +356,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } private void disableTrackSelectionsInResult() { - TrackSelectorResult trackSelectorResult = this.trackSelectorResult; - if (!isLoadingMediaPeriod() || trackSelectorResult == null) { + if (!isLoadingMediaPeriod()) { return; } for (int i = 0; i < trackSelectorResult.length; i++) { @@ -394,7 +387,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; */ private void associateNoSampleRenderersWithEmptySampleStream( @NullableType SampleStream[] sampleStreams) { - TrackSelectorResult trackSelectorResult = Assertions.checkNotNull(this.trackSelectorResult); for (int i = 0; i < rendererCapabilities.length; i++) { if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE && trackSelectorResult.isRendererEnabled(i)) { 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 9c0dd80a10..0f279ba6d3 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 @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; @@ -135,13 +136,16 @@ import com.google.android.exoplayer2.util.Assertions; * @param allocator The allocator. * @param mediaSource The media source that produced the media period. * @param info Information used to identify this media period in its timeline period. + * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each + * renderer. */ public MediaPeriod enqueueNextMediaPeriod( RendererCapabilities[] rendererCapabilities, TrackSelector trackSelector, Allocator allocator, MediaSource mediaSource, - MediaPeriodInfo info) { + MediaPeriodInfo info, + TrackSelectorResult emptyTrackSelectorResult) { long rendererPositionOffsetUs = loading == null ? (info.id.isAd() && info.contentPositionUs != C.TIME_UNSET @@ -155,7 +159,8 @@ import com.google.android.exoplayer2.util.Assertions; trackSelector, allocator, mediaSource, - info); + info, + emptyTrackSelectorResult); if (loading != null) { Assertions.checkState(hasPlayingPeriod()); loading.setNext(newPeriodHolder); 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 fc723134f7..9228f3af62 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 @@ -85,8 +85,8 @@ public final class TrackSelectorResult { /** * Returns whether this result is equivalent to {@code other} for the renderer at the given index. - * The results are equivalent if they have equal renderersEnabled array, track selections, and - * configurations for the renderer. + * The results are equivalent if they have equal track selections and configurations for the + * renderer. * * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false} * will be returned. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java index def7f8552e..14aa436be3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java @@ -26,7 +26,9 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline; +import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.upstream.Allocator; import org.junit.Before; import org.junit.Test; @@ -381,7 +383,13 @@ public final class MediaPeriodQueueTest { private void enqueueNext() { mediaPeriodQueue.enqueueNextMediaPeriod( - rendererCapabilities, trackSelector, allocator, mediaSource, getNextMediaPeriodInfo()); + rendererCapabilities, + trackSelector, + allocator, + mediaSource, + getNextMediaPeriodInfo(), + new TrackSelectorResult( + new RendererConfiguration[0], new TrackSelection[0], /* info= */ null)); } private MediaPeriodInfo getNextMediaPeriodInfo() { From 39317048e975042b34dde82283018f41d4880274 Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Fri, 2 Aug 2019 10:27:35 +0100 Subject: [PATCH 1521/1556] Add video decoder exception class This will be used in common video renderer and decoder classes. PiperOrigin-RevId: 261287124 --- .../ext/vp9/VpxDecoderException.java | 4 +- .../video/VideoDecoderException.java | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderException.java diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java index 8de14629d3..b2da9a7ff8 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2.ext.vp9; +import com.google.android.exoplayer2.video.VideoDecoderException; + /** Thrown when a libvpx decoder error occurs. */ -public final class VpxDecoderException extends Exception { +public final class VpxDecoderException extends VideoDecoderException { /* package */ VpxDecoderException(String message) { super(message); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderException.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderException.java new file mode 100644 index 0000000000..68108af636 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderException.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2019 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.video; + +/** Thrown when a video decoder error occurs. */ +public class VideoDecoderException extends Exception { + + /** + * Creates an instance with the given message. + * + * @param message The detail message for this exception. + */ + public VideoDecoderException(String message) { + super(message); + } + + /** + * Creates an instance with the given message and cause. + * + * @param message The detail message for this exception. + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). + * A null value is permitted, and indicates that the cause is nonexistent or unknown. + */ + public VideoDecoderException(String message, Throwable cause) { + super(message, cause); + } +} From 4482db40e1b96e5ead3258cec90a738883ee1f5e Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Fri, 2 Aug 2019 11:44:48 +0100 Subject: [PATCH 1522/1556] Move output modes to constants file PiperOrigin-RevId: 261295173 --- .../ext/vp9/LibvpxVideoRenderer.java | 23 ++++++++++--------- .../exoplayer2/ext/vp9/VpxDecoder.java | 13 ++++------- .../exoplayer2/ext/vp9/VpxOutputBuffer.java | 10 ++++---- .../java/com/google/android/exoplayer2/C.java | 15 ++++++++++++ 4 files changed, 37 insertions(+), 24 deletions(-) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index 34301742e5..b6663ac3d7 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -137,7 +137,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { private long joiningDeadlineMs; private Surface surface; private VpxOutputBufferRenderer outputBufferRenderer; - private int outputMode; + @C.VideoOutputMode private int outputMode; private boolean waitingForKeys; private boolean inputStreamEnded; @@ -275,7 +275,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { formatQueue = new TimedValueQueue<>(); flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); eventDispatcher = new EventDispatcher(eventHandler, eventListener); - outputMode = VpxDecoder.OUTPUT_MODE_NONE; + outputMode = C.VIDEO_OUTPUT_MODE_NONE; decoderReinitializationState = REINITIALIZATION_STATE_NONE; } @@ -349,8 +349,9 @@ public class LibvpxVideoRenderer extends BaseRenderer { if (waitingForKeys) { return false; } - if (format != null && (isSourceReady() || outputBuffer != null) - && (renderedFirstFrame || outputMode == VpxDecoder.OUTPUT_MODE_NONE)) { + if (format != null + && (isSourceReady() || outputBuffer != null) + && (renderedFirstFrame || outputMode == C.VIDEO_OUTPUT_MODE_NONE)) { // Ready. If we were joining then we've now joined, so clear the joining deadline. joiningDeadlineMs = C.TIME_UNSET; return true; @@ -628,8 +629,8 @@ public class LibvpxVideoRenderer extends BaseRenderer { */ protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) throws VpxDecoderException { int bufferMode = outputBuffer.mode; - boolean renderSurface = bufferMode == VpxDecoder.OUTPUT_MODE_SURFACE_YUV && surface != null; - boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null; + boolean renderSurface = bufferMode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && surface != null; + boolean renderYuv = bufferMode == C.VIDEO_OUTPUT_MODE_YUV && outputBufferRenderer != null; lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; if (!renderYuv && !renderSurface) { dropOutputBuffer(outputBuffer); @@ -713,12 +714,12 @@ public class LibvpxVideoRenderer extends BaseRenderer { this.surface = surface; this.outputBufferRenderer = outputBufferRenderer; if (surface != null) { - outputMode = VpxDecoder.OUTPUT_MODE_SURFACE_YUV; + outputMode = C.VIDEO_OUTPUT_MODE_SURFACE_YUV; } else { outputMode = - outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV : VpxDecoder.OUTPUT_MODE_NONE; + outputBufferRenderer != null ? C.VIDEO_OUTPUT_MODE_YUV : C.VIDEO_OUTPUT_MODE_NONE; } - if (outputMode != VpxDecoder.OUTPUT_MODE_NONE) { + if (outputMode != C.VIDEO_OUTPUT_MODE_NONE) { if (decoder != null) { decoder.setOutputMode(outputMode); } @@ -735,7 +736,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { clearReportedVideoSize(); clearRenderedFirstFrame(); } - } else if (outputMode != VpxDecoder.OUTPUT_MODE_NONE) { + } else if (outputMode != C.VIDEO_OUTPUT_MODE_NONE) { // The output is unchanged and non-null. If we know the video size and/or have already // rendered to the output, report these again immediately. maybeRenotifyVideoSizeChanged(); @@ -915,7 +916,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { } long earlyUs = outputBuffer.timeUs - positionUs; - if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) { + if (outputMode == C.VIDEO_OUTPUT_MODE_NONE) { // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. if (isBufferLate(earlyUs)) { skipOutputBuffer(outputBuffer); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 544259ffc0..93a4a2fc1f 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -29,10 +29,6 @@ import java.nio.ByteBuffer; /* package */ final class VpxDecoder extends SimpleDecoder { - public static final int OUTPUT_MODE_NONE = -1; - public static final int OUTPUT_MODE_YUV = 0; - public static final int OUTPUT_MODE_SURFACE_YUV = 1; - private static final int NO_ERROR = 0; private static final int DECODE_ERROR = 1; private static final int DRM_ERROR = 2; @@ -40,7 +36,7 @@ import java.nio.ByteBuffer; private final ExoMediaCrypto exoMediaCrypto; private final long vpxDecContext; - private volatile int outputMode; + @C.VideoOutputMode private volatile int outputMode; /** * Creates a VP9 decoder. @@ -87,10 +83,9 @@ import java.nio.ByteBuffer; /** * Sets the output mode for frames rendered by the decoder. * - * @param outputMode The output mode. One of {@link #OUTPUT_MODE_NONE} and {@link - * #OUTPUT_MODE_YUV}. + * @param outputMode The output mode. */ - public void setOutputMode(int outputMode) { + public void setOutputMode(@C.VideoOutputMode int outputMode) { this.outputMode = outputMode; } @@ -108,7 +103,7 @@ import java.nio.ByteBuffer; protected void releaseOutputBuffer(VpxOutputBuffer buffer) { // Decode only frames do not acquire a reference on the internal decoder buffer and thus do not // require a call to vpxReleaseFrame. - if (outputMode == OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) { + if (outputMode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) { vpxReleaseFrame(vpxDecContext, buffer); } super.releaseOutputBuffer(buffer); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java index 30d7b8e92c..de411089ab 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.vp9; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.OutputBuffer; import com.google.android.exoplayer2.video.ColorInfo; import java.nio.ByteBuffer; @@ -31,7 +32,8 @@ public final class VpxOutputBuffer extends OutputBuffer { /** Decoder private data. */ public int decoderPrivate; - public int mode; + /** Output mode. */ + @C.VideoOutputMode public int mode; /** * RGB buffer for RGB mode. */ @@ -60,10 +62,10 @@ public final class VpxOutputBuffer extends OutputBuffer { * Initializes the buffer. * * @param timeUs The presentation timestamp for the buffer, in microseconds. - * @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE}, {@link - * VpxDecoder#OUTPUT_MODE_YUV} and {@link VpxDecoder#OUTPUT_MODE_SURFACE_YUV}. + * @param mode The output mode. One of {@link C#VIDEO_OUTPUT_MODE_NONE}, {@link + * C#VIDEO_OUTPUT_MODE_YUV} and {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV}. */ - public void init(long timeUs, int mode) { + public void init(long timeUs, @C.VideoOutputMode int mode) { this.timeUs = timeUs; this.mode = mode; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 8ded5038b0..9ed5cb7e36 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -499,6 +499,21 @@ public final class C { /** Indicates that a buffer should be decoded but not rendered. */ public static final int BUFFER_FLAG_DECODE_ONLY = 1 << 31; // 0x80000000 + /** + * Video decoder output modes. Possible modes are {@link #VIDEO_OUTPUT_MODE_NONE}, {@link + * #VIDEO_OUTPUT_MODE_YUV} and {@link #VIDEO_OUTPUT_MODE_SURFACE_YUV}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = {VIDEO_OUTPUT_MODE_NONE, VIDEO_OUTPUT_MODE_YUV, VIDEO_OUTPUT_MODE_SURFACE_YUV}) + public @interface VideoOutputMode {} + /** Video decoder output mode is not set. */ + public static final int VIDEO_OUTPUT_MODE_NONE = -1; + /** Video decoder output mode that outputs raw 4:2:0 YUV planes. */ + public static final int VIDEO_OUTPUT_MODE_YUV = 0; + /** Video decoder output mode that renders 4:2:0 YUV planes directly to a surface. */ + public static final int VIDEO_OUTPUT_MODE_SURFACE_YUV = 1; + /** * Video scaling modes for {@link MediaCodec}-based {@link Renderer}s. One of {@link * #VIDEO_SCALING_MODE_SCALE_TO_FIT} or {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}. From 851218aca931c855247acef48eaa791daa3aae7c Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 2 Aug 2019 14:20:15 +0100 Subject: [PATCH 1523/1556] Fix AnalyticsCollectorTest flakiness. Two tests have very low propability flakiness (1:1000) due to not waiting for a seek in one case and the chance of already being ended in another case. Fix these and also adjust wrong comments about state changes. PiperOrigin-RevId: 261309976 --- .../exoplayer2/analytics/AnalyticsCollectorTest.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index a2546adfe4..875f8b5d7b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -368,8 +368,7 @@ public final class AnalyticsCollectorTest { .pause() .waitForPlaybackState(Player.STATE_READY) .playUntilPosition(/* windowIndex= */ 0, periodDurationMs) - .seek(/* positionMs= */ 0) - .waitForPlaybackState(Player.STATE_READY) + .seekAndWait(/* positionMs= */ 0) .play() .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); @@ -378,8 +377,8 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady=true */, - WINDOW_0 /* BUFFERING */, WINDOW_0 /* setPlayWhenReady=false */, + WINDOW_0 /* BUFFERING */, period0 /* READY */, period0 /* setPlayWhenReady=true */, period0 /* setPlayWhenReady=false */, @@ -505,6 +504,7 @@ public final class AnalyticsCollectorTest { .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) + .seek(/* positionMs= */ 0) .prepareSource(mediaSource, /* resetPosition= */ false, /* resetState= */ false) .waitForPlaybackState(Player.STATE_ENDED) .build(); @@ -522,6 +522,9 @@ public final class AnalyticsCollectorTest { period0Seq0 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* prepared */); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0Seq0, period0Seq0, period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period0Seq0); @@ -585,8 +588,8 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady=true */, - WINDOW_0 /* BUFFERING */, WINDOW_0 /* setPlayWhenReady=false */, + WINDOW_0 /* BUFFERING */, window0Period1Seq0 /* READY */, window0Period1Seq0 /* setPlayWhenReady=true */, window0Period1Seq0 /* setPlayWhenReady=false */, From 90ab05c574dd2f9b2ffb635fc3c3c4128040191d Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 14:42:14 +0100 Subject: [PATCH 1524/1556] Add DRM samples to Cast demo app PiperOrigin-RevId: 261312509 --- .../android/exoplayer2/castdemo/DemoUtil.java | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index 91ea0c92e2..dacdbfe616 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -36,7 +36,6 @@ import java.util.List; public static final List SAMPLES; static { - // App samples. ArrayList samples = new ArrayList<>(); // Clear content. @@ -59,6 +58,45 @@ import java.util.List; .setMimeType(MIME_TYPE_VIDEO_MP4) .build()); + // DRM content. + samples.add( + new MediaItem.Builder() + .setUri(Uri.parse("https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd")) + .setTitle("Widevine DASH cenc: Tears") + .setMimeType(MIME_TYPE_DASH) + .setDrmConfiguration( + new DrmConfiguration( + C.WIDEVINE_UUID, + Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"), + Collections.emptyMap())) + .build()); + samples.add( + new MediaItem.Builder() + .setUri( + Uri.parse( + "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd")) + .setTitle("Widevine DASH cbc1: Tears") + .setMimeType(MIME_TYPE_DASH) + .setDrmConfiguration( + new DrmConfiguration( + C.WIDEVINE_UUID, + Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"), + Collections.emptyMap())) + .build()); + samples.add( + new MediaItem.Builder() + .setUri( + Uri.parse( + "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd")) + .setTitle("Widevine DASH cbcs: Tears") + .setMimeType(MIME_TYPE_DASH) + .setDrmConfiguration( + new DrmConfiguration( + C.WIDEVINE_UUID, + Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"), + Collections.emptyMap())) + .build()); + SAMPLES = Collections.unmodifiableList(samples); } From 91c62ea26f99f04c67fde77cf7380f21d0729e54 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 15:20:35 +0100 Subject: [PATCH 1525/1556] Fix DefaultOggSeeker seeking - When in STATE_SEEK with targetGranule==0, seeking would exit without checking that the input was positioned at the correct place. - Seeking could fail due to trying to read beyond the end of the stream. - Seeking was not robust against IO errors during the skip phase that occurs after the binary search has sufficiently converged. PiperOrigin-RevId: 261317035 --- .../extractor/ogg/DefaultOggSeeker.java | 177 ++++++++---------- .../extractor/ogg/StreamReader.java | 2 +- .../extractor/ogg/DefaultOggSeekerTest.java | 107 +++++------ .../ogg/DefaultOggSeekerUtilMethodsTest.java | 95 +--------- 4 files changed, 129 insertions(+), 252 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index 308547e510..064bd5732d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ogg; import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; @@ -35,11 +36,12 @@ import java.io.IOException; private static final int STATE_SEEK_TO_END = 0; private static final int STATE_READ_LAST_PAGE = 1; private static final int STATE_SEEK = 2; - private static final int STATE_IDLE = 3; + private static final int STATE_SKIP = 3; + private static final int STATE_IDLE = 4; private final OggPageHeader pageHeader = new OggPageHeader(); - private final long startPosition; - private final long endPosition; + private final long payloadStartPosition; + private final long payloadEndPosition; private final StreamReader streamReader; private int state; @@ -55,26 +57,27 @@ import java.io.IOException; /** * Constructs an OggSeeker. * - * @param startPosition Start position of the payload (inclusive). - * @param endPosition End position of the payload (exclusive). * @param streamReader The {@link StreamReader} that owns this seeker. + * @param payloadStartPosition Start position of the payload (inclusive). + * @param payloadEndPosition End position of the payload (exclusive). * @param firstPayloadPageSize The total size of the first payload page, in bytes. * @param firstPayloadPageGranulePosition The granule position of the first payload page. - * @param firstPayloadPageIsLastPage Whether the first payload page is also the last page in the - * ogg stream. + * @param firstPayloadPageIsLastPage Whether the first payload page is also the last page. */ public DefaultOggSeeker( - long startPosition, - long endPosition, StreamReader streamReader, + long payloadStartPosition, + long payloadEndPosition, long firstPayloadPageSize, long firstPayloadPageGranulePosition, boolean firstPayloadPageIsLastPage) { - Assertions.checkArgument(startPosition >= 0 && endPosition > startPosition); + Assertions.checkArgument( + payloadStartPosition >= 0 && payloadEndPosition > payloadStartPosition); this.streamReader = streamReader; - this.startPosition = startPosition; - this.endPosition = endPosition; - if (firstPayloadPageSize == endPosition - startPosition || firstPayloadPageIsLastPage) { + this.payloadStartPosition = payloadStartPosition; + this.payloadEndPosition = payloadEndPosition; + if (firstPayloadPageSize == payloadEndPosition - payloadStartPosition + || firstPayloadPageIsLastPage) { totalGranules = firstPayloadPageGranulePosition; state = STATE_IDLE; } else { @@ -91,7 +94,7 @@ import java.io.IOException; positionBeforeSeekToEnd = input.getPosition(); state = STATE_READ_LAST_PAGE; // Seek to the end just before the last page of stream to get the duration. - long lastPageSearchPosition = endPosition - OggPageHeader.MAX_PAGE_SIZE; + long lastPageSearchPosition = payloadEndPosition - OggPageHeader.MAX_PAGE_SIZE; if (lastPageSearchPosition > positionBeforeSeekToEnd) { return lastPageSearchPosition; } @@ -101,137 +104,110 @@ import java.io.IOException; state = STATE_IDLE; return positionBeforeSeekToEnd; case STATE_SEEK: - long currentGranule; - if (targetGranule == 0) { - currentGranule = 0; - } else { - long position = getNextSeekPosition(targetGranule, input); - if (position >= 0) { - return position; - } - currentGranule = skipToPageOfGranule(input, targetGranule, -(position + 2)); + long position = getNextSeekPosition(input); + if (position != C.POSITION_UNSET) { + return position; } + state = STATE_SKIP; + // Fall through. + case STATE_SKIP: + skipToPageOfTargetGranule(input); state = STATE_IDLE; - return -(currentGranule + 2); + return -(startGranule + 2); default: // Never happens. throw new IllegalStateException(); } } - @Override - public void startSeek(long targetGranule) { - Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK); - this.targetGranule = targetGranule; - state = STATE_SEEK; - resetSeeking(); - } - @Override public OggSeekMap createSeekMap() { return totalGranules != 0 ? new OggSeekMap() : null; } - @VisibleForTesting - public void resetSeeking() { - start = startPosition; - end = endPosition; + @Override + public void startSeek(long targetGranule) { + this.targetGranule = targetGranule; + state = STATE_SEEK; + start = payloadStartPosition; + end = payloadEndPosition; startGranule = 0; endGranule = totalGranules; } /** - * Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput} - * has to seek and then be passed for another call until a negative number is returned. If a - * negative number is returned the input is at a position which is before the target page and at - * which it is sensible to just skip pages to the target granule and pre-roll instead of doing - * another seek request. + * Performs a single step of a seeking binary search, returning the byte position from which data + * should be provided for the next step, or {@link C#POSITION_UNSET} if the search has converged. + * If the search has converged then {@link #skipToPageOfTargetGranule(ExtractorInput)} should be + * called to skip to the target page. * - * @param targetGranule The target granule position to seek to. * @param input The {@link ExtractorInput} to read from. - * @return The position to seek the {@link ExtractorInput} to for a next call or -(currentGranule - * + 2) if it's close enough to skip to the target page. + * @return The byte position from which data should be provided for the next step, or {@link + * C#POSITION_UNSET} if the search has converged. * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from the input. */ - @VisibleForTesting - public long getNextSeekPosition(long targetGranule, ExtractorInput input) - throws IOException, InterruptedException { + private long getNextSeekPosition(ExtractorInput input) throws IOException, InterruptedException { if (start == end) { - return -(startGranule + 2); + return C.POSITION_UNSET; } - long initialPosition = input.getPosition(); + long currentPosition = input.getPosition(); if (!skipToNextPage(input, end)) { - if (start == initialPosition) { + if (start == currentPosition) { throw new IOException("No ogg page can be found."); } return start; } - pageHeader.populate(input, false); + pageHeader.populate(input, /* quiet= */ false); input.resetPeekPosition(); long granuleDistance = targetGranule - pageHeader.granulePosition; int pageSize = pageHeader.headerSize + pageHeader.bodySize; - if (granuleDistance < 0 || granuleDistance > MATCH_RANGE) { - if (granuleDistance < 0) { - end = initialPosition; - endGranule = pageHeader.granulePosition; - } else { - start = input.getPosition() + pageSize; - startGranule = pageHeader.granulePosition; - if (end - start + pageSize < MATCH_BYTE_RANGE) { - input.skipFully(pageSize); - return -(startGranule + 2); - } - } - - if (end - start < MATCH_BYTE_RANGE) { - end = start; - return start; - } - - long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L); - long nextPosition = input.getPosition() - offset - + (granuleDistance * (end - start) / (endGranule - startGranule)); - - nextPosition = Math.max(nextPosition, start); - nextPosition = Math.min(nextPosition, end - 1); - return nextPosition; + if (0 <= granuleDistance && granuleDistance < MATCH_RANGE) { + return C.POSITION_UNSET; } - // position accepted (before target granule and within MATCH_RANGE) - input.skipFully(pageSize); - return -(pageHeader.granulePosition + 2); + if (granuleDistance < 0) { + end = currentPosition; + endGranule = pageHeader.granulePosition; + } else { + start = input.getPosition() + pageSize; + startGranule = pageHeader.granulePosition; + } + + if (end - start < MATCH_BYTE_RANGE) { + end = start; + return start; + } + + long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L); + long nextPosition = + input.getPosition() + - offset + + (granuleDistance * (end - start) / (endGranule - startGranule)); + return Util.constrainValue(nextPosition, start, end - 1); } /** - * Skips to the position of the start of the page containing the {@code targetGranule} and returns - * the granule of the page previous to the target page. + * Skips forward to the start of the page containing the {@code targetGranule}. * * @param input The {@link ExtractorInput} to read from. - * @param targetGranule The target granule. - * @param currentGranule The current granule or -1 if it's unknown. - * @return The granule of the prior page or the {@code currentGranule} if there isn't a prior - * page. * @throws ParserException If populating the page header fails. * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from the input. */ - @VisibleForTesting - long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule) + private void skipToPageOfTargetGranule(ExtractorInput input) throws IOException, InterruptedException { - pageHeader.populate(input, false); + pageHeader.populate(input, /* quiet= */ false); while (pageHeader.granulePosition < targetGranule) { input.skipFully(pageHeader.headerSize + pageHeader.bodySize); - // Store in a member field to be able to resume after IOExceptions. - currentGranule = pageHeader.granulePosition; - // Peek next header. - pageHeader.populate(input, false); + start = input.getPosition(); + startGranule = pageHeader.granulePosition; + pageHeader.populate(input, /* quiet= */ false); } input.resetPeekPosition(); - return currentGranule; } /** @@ -244,7 +220,7 @@ import java.io.IOException; */ @VisibleForTesting void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException { - if (!skipToNextPage(input, endPosition)) { + if (!skipToNextPage(input, payloadEndPosition)) { // Not found until eof. throw new EOFException(); } @@ -261,7 +237,7 @@ import java.io.IOException; */ private boolean skipToNextPage(ExtractorInput input, long limit) throws IOException, InterruptedException { - limit = Math.min(limit + 3, endPosition); + limit = Math.min(limit + 3, payloadEndPosition); byte[] buffer = new byte[2048]; int peekLength = buffer.length; while (true) { @@ -302,8 +278,8 @@ import java.io.IOException; long readGranuleOfLastPage(ExtractorInput input) throws IOException, InterruptedException { skipToNextPage(input); pageHeader.reset(); - while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < endPosition) { - pageHeader.populate(input, false); + while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < payloadEndPosition) { + pageHeader.populate(input, /* quiet= */ false); input.skipFully(pageHeader.headerSize + pageHeader.bodySize); } return pageHeader.granulePosition; @@ -320,10 +296,11 @@ import java.io.IOException; public SeekPoints getSeekPoints(long timeUs) { long targetGranule = streamReader.convertTimeToGranule(timeUs); long estimatedPosition = - startPosition - + (targetGranule * (endPosition - startPosition) / totalGranules) + payloadStartPosition + + (targetGranule * (payloadEndPosition - payloadStartPosition) / totalGranules) - DEFAULT_OFFSET; - estimatedPosition = Util.constrainValue(estimatedPosition, startPosition, endPosition - 1); + estimatedPosition = + Util.constrainValue(estimatedPosition, payloadStartPosition, payloadEndPosition - 1); return new SeekPoints(new SeekPoint(timeUs, estimatedPosition)); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java index 35a07fcf49..d2671125e4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java @@ -148,9 +148,9 @@ import java.io.IOException; boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream. oggSeeker = new DefaultOggSeeker( + this, payloadStartPosition, input.getLength(), - this, firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize, firstPayloadPageHeader.granulePosition, isLastPage); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java index 8d1818845d..fba358ea51 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.extractor.ogg; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.fail; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -36,9 +35,9 @@ public final class DefaultOggSeekerTest { public void testSetupWithUnsetEndPositionFails() { try { new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ C.LENGTH_UNSET, /* streamReader= */ new TestStreamReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ C.LENGTH_UNSET, /* firstPayloadPageSize= */ 1, /* firstPayloadPageGranulePosition= */ 1, /* firstPayloadPageIsLastPage= */ false); @@ -62,9 +61,9 @@ public final class DefaultOggSeekerTest { TestStreamReader streamReader = new TestStreamReader(); DefaultOggSeeker oggSeeker = new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ testFile.data.length, /* streamReader= */ streamReader, + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ testFile.data.length, /* firstPayloadPageSize= */ testFile.firstPayloadPageSize, /* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranulePosition, /* firstPayloadPageIsLastPage= */ false); @@ -78,70 +77,56 @@ public final class DefaultOggSeekerTest { input.setPosition((int) nextSeekPosition); } - // Test granule 0 from file start - assertThat(seekTo(input, oggSeeker, 0, 0)).isEqualTo(0); + // Test granule 0 from file start. + long granule = seekTo(input, oggSeeker, 0, 0); + assertThat(granule).isEqualTo(0); assertThat(input.getPosition()).isEqualTo(0); - // Test granule 0 from file end - assertThat(seekTo(input, oggSeeker, 0, testFile.data.length - 1)).isEqualTo(0); + // Test granule 0 from file end. + granule = seekTo(input, oggSeeker, 0, testFile.data.length - 1); + assertThat(granule).isEqualTo(0); assertThat(input.getPosition()).isEqualTo(0); - { // Test last granule - long currentGranule = seekTo(input, oggSeeker, testFile.lastGranule, 0); - long position = testFile.data.length; - assertThat( - (testFile.lastGranule > currentGranule && position > input.getPosition()) - || (testFile.lastGranule == currentGranule && position == input.getPosition())) - .isTrue(); - } + // Test last granule. + granule = seekTo(input, oggSeeker, testFile.lastGranule, 0); + long position = testFile.data.length; + // TODO: Simplify this. + assertThat( + (testFile.lastGranule > granule && position > input.getPosition()) + || (testFile.lastGranule == granule && position == input.getPosition())) + .isTrue(); - { // Test exact granule - input.setPosition(testFile.data.length / 2); - oggSeeker.skipToNextPage(input); - assertThat(pageHeader.populate(input, true)).isTrue(); - long position = input.getPosition() + pageHeader.headerSize + pageHeader.bodySize; - long currentGranule = seekTo(input, oggSeeker, pageHeader.granulePosition, 0); - assertThat( - (pageHeader.granulePosition > currentGranule && position > input.getPosition()) - || (pageHeader.granulePosition == currentGranule - && position == input.getPosition())) - .isTrue(); - } + // Test exact granule. + input.setPosition(testFile.data.length / 2); + oggSeeker.skipToNextPage(input); + assertThat(pageHeader.populate(input, true)).isTrue(); + position = input.getPosition() + pageHeader.headerSize + pageHeader.bodySize; + granule = seekTo(input, oggSeeker, pageHeader.granulePosition, 0); + // TODO: Simplify this. + assertThat( + (pageHeader.granulePosition > granule && position > input.getPosition()) + || (pageHeader.granulePosition == granule && position == input.getPosition())) + .isTrue(); for (int i = 0; i < 100; i += 1) { long targetGranule = (long) (random.nextDouble() * testFile.lastGranule); int initialPosition = random.nextInt(testFile.data.length); - - long currentGranule = seekTo(input, oggSeeker, targetGranule, initialPosition); + granule = seekTo(input, oggSeeker, targetGranule, initialPosition); long currentPosition = input.getPosition(); - - assertWithMessage("getNextSeekPosition() didn't leave input on a page start.") - .that(pageHeader.populate(input, true)) - .isTrue(); - - if (currentGranule == 0) { + if (granule == 0) { assertThat(currentPosition).isEqualTo(0); } else { int previousPageStart = testFile.findPreviousPageStart(currentPosition); input.setPosition(previousPageStart); - assertThat(pageHeader.populate(input, true)).isTrue(); - assertThat(currentGranule).isEqualTo(pageHeader.granulePosition); + pageHeader.populate(input, false); + assertThat(granule).isEqualTo(pageHeader.granulePosition); } input.setPosition((int) currentPosition); - oggSeeker.skipToPageOfGranule(input, targetGranule, -1); - long positionDiff = Math.abs(input.getPosition() - currentPosition); - - long granuleDiff = currentGranule - targetGranule; - if ((granuleDiff > DefaultOggSeeker.MATCH_RANGE || granuleDiff < 0) - && positionDiff > DefaultOggSeeker.MATCH_BYTE_RANGE) { - fail( - "granuleDiff (" - + granuleDiff - + ") or positionDiff (" - + positionDiff - + ") is more than allowed."); - } + pageHeader.populate(input, false); + // The target granule should be within the current page. + assertThat(granule).isAtMost(targetGranule); + assertThat(targetGranule).isLessThan(pageHeader.granulePosition); } } @@ -149,18 +134,15 @@ public final class DefaultOggSeekerTest { FakeExtractorInput input, DefaultOggSeeker oggSeeker, long targetGranule, int initialPosition) throws IOException, InterruptedException { long nextSeekPosition = initialPosition; + oggSeeker.startSeek(targetGranule); int count = 0; - oggSeeker.resetSeeking(); - - do { - input.setPosition((int) nextSeekPosition); - nextSeekPosition = oggSeeker.getNextSeekPosition(targetGranule, input); - + while (nextSeekPosition >= 0) { if (count++ > 100) { - fail("infinite loop?"); + fail("Seek failed to converge in 100 iterations"); } - } while (nextSeekPosition >= 0); - + input.setPosition((int) nextSeekPosition); + nextSeekPosition = oggSeeker.read(input); + } return -(nextSeekPosition + 2); } @@ -171,8 +153,7 @@ public final class DefaultOggSeekerTest { } @Override - protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) - throws IOException, InterruptedException { + protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) { return false; } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java index d6691f50f8..2521602228 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java @@ -85,9 +85,9 @@ public final class DefaultOggSeekerUtilMethodsTest { throws IOException, InterruptedException { DefaultOggSeeker oggSeeker = new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ extractorInput.getLength(), /* streamReader= */ new FlacReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ extractorInput.getLength(), /* firstPayloadPageSize= */ 1, /* firstPayloadPageGranulePosition= */ 2, /* firstPayloadPageIsLastPage= */ false); @@ -99,87 +99,6 @@ public final class DefaultOggSeekerUtilMethodsTest { } } - @Test - public void testSkipToPageOfGranule() throws IOException, InterruptedException { - byte[] packet = TestUtil.buildTestData(3 * 254, random); - byte[] data = TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet); - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); - - // expect to be granule of the previous page returned as elapsedSamples - skipToPageOfGranule(input, 54000, 40000); - // expect to be at the start of the third page - assertThat(input.getPosition()).isEqualTo(2 * (30 + (3 * 254))); - } - - @Test - public void testSkipToPageOfGranulePreciseMatch() throws IOException, InterruptedException { - byte[] packet = TestUtil.buildTestData(3 * 254, random); - byte[] data = TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet); - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); - - skipToPageOfGranule(input, 40000, 20000); - // expect to be at the start of the second page - assertThat(input.getPosition()).isEqualTo(30 + (3 * 254)); - } - - @Test - public void testSkipToPageOfGranuleAfterTargetPage() throws IOException, InterruptedException { - byte[] packet = TestUtil.buildTestData(3 * 254, random); - byte[] data = TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet); - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); - - skipToPageOfGranule(input, 10000, -1); - assertThat(input.getPosition()).isEqualTo(0); - } - - private void skipToPageOfGranule(ExtractorInput input, long granule, - long elapsedSamplesExpected) throws IOException, InterruptedException { - DefaultOggSeeker oggSeeker = - new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ input.getLength(), - /* streamReader= */ new FlacReader(), - /* firstPayloadPageSize= */ 1, - /* firstPayloadPageGranulePosition= */ 2, - /* firstPayloadPageIsLastPage= */ false); - while (true) { - try { - assertThat(oggSeeker.skipToPageOfGranule(input, granule, -1)) - .isEqualTo(elapsedSamplesExpected); - return; - } catch (FakeExtractorInput.SimulatedIOException e) { - input.resetPeekPosition(); - } - } - } - @Test public void testReadGranuleOfLastPage() throws IOException, InterruptedException { FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays( @@ -204,7 +123,7 @@ public final class DefaultOggSeekerUtilMethodsTest { assertReadGranuleOfLastPage(input, 60000); fail(); } catch (EOFException e) { - // ignored + // Ignored. } } @@ -216,7 +135,7 @@ public final class DefaultOggSeekerUtilMethodsTest { assertReadGranuleOfLastPage(input, 60000); fail(); } catch (IllegalArgumentException e) { - // ignored + // Ignored. } } @@ -224,9 +143,9 @@ public final class DefaultOggSeekerUtilMethodsTest { throws IOException, InterruptedException { DefaultOggSeeker oggSeeker = new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ input.getLength(), /* streamReader= */ new FlacReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ input.getLength(), /* firstPayloadPageSize= */ 1, /* firstPayloadPageGranulePosition= */ 2, /* firstPayloadPageIsLastPage= */ false); @@ -235,7 +154,7 @@ public final class DefaultOggSeekerUtilMethodsTest { assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected); break; } catch (FakeExtractorInput.SimulatedIOException e) { - // ignored + // Ignored. } } } From 5e98d76e8b5ca816006400fb2e84e44b4d3a90c4 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 15:29:12 +0100 Subject: [PATCH 1526/1556] Improve extractor tests based on ExtractorAsserts - Test seeking to (timeUs=0, position=0), which should always work and produce the same output as initially reading from the start of the stream. - Reset the input when testing seeking, to ensure IO errors are simulated for this case. PiperOrigin-RevId: 261317898 --- .../exoplayer2/testutil/ExtractorAsserts.java | 17 +++++++++++++---- .../exoplayer2/testutil/FakeExtractorInput.java | 9 +++++++++ .../testutil/FakeExtractorOutput.java | 6 ++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java index 3937dabcaf..a933121bc5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java @@ -175,17 +175,26 @@ public final class ExtractorAsserts { extractorOutput.assertOutput(context, file + ".0" + DUMP_EXTENSION); } + // Seeking to (timeUs=0, position=0) should always work, and cause the same data to be output. + extractorOutput.clearTrackOutputs(); + input.reset(); + consumeTestData(extractor, input, /* timeUs= */ 0, extractorOutput, false); + if (simulateUnknownLength && assetExists(context, file + UNKNOWN_LENGTH_EXTENSION)) { + extractorOutput.assertOutput(context, file + UNKNOWN_LENGTH_EXTENSION); + } else { + extractorOutput.assertOutput(context, file + ".0" + DUMP_EXTENSION); + } + + // If the SeekMap is seekable, test seeking to 4 positions in the stream. SeekMap seekMap = extractorOutput.seekMap; if (seekMap.isSeekable()) { long durationUs = seekMap.getDurationUs(); for (int j = 0; j < 4; j++) { + extractorOutput.clearTrackOutputs(); long timeUs = (durationUs * j) / 3; long position = seekMap.getSeekPoints(timeUs).first.position; + input.reset(); input.setPosition((int) position); - for (int i = 0; i < extractorOutput.numberOfTracks; i++) { - extractorOutput.trackOutputs.valueAt(i).clear(); - } - consumeTestData(extractor, input, timeUs, extractorOutput, false); extractorOutput.assertOutput(context, file + '.' + j + DUMP_EXTENSION); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java index 20f6f436b0..443ffdb12c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java @@ -80,6 +80,15 @@ public final class FakeExtractorInput implements ExtractorInput { failedPeekPositions = new SparseBooleanArray(); } + /** Resets the input to its initial state. */ + public void reset() { + readPosition = 0; + peekPosition = 0; + partiallySatisfiedTargetPositions.clear(); + failedReadPositions.clear(); + failedPeekPositions.clear(); + } + /** * Sets the read and peek positions. * diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java index c6543bd7a5..4022a0ccc1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java @@ -70,6 +70,12 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab this.seekMap = seekMap; } + public void clearTrackOutputs() { + for (int i = 0; i < numberOfTracks; i++) { + trackOutputs.valueAt(i).clear(); + } + } + public void assertEquals(FakeExtractorOutput expected) { assertThat(numberOfTracks).isEqualTo(expected.numberOfTracks); assertThat(tracksEnded).isEqualTo(expected.tracksEnded); From 173eadc70ed0d63eeab7935d4d3ee3c58ea89fc1 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 15:48:44 +0100 Subject: [PATCH 1527/1556] Move DefaultOggSeeker tests into a single class PiperOrigin-RevId: 261320318 --- .../extractor/ogg/DefaultOggSeekerTest.java | 137 ++++++++++++++- .../ogg/DefaultOggSeekerUtilMethodsTest.java | 162 ------------------ 2 files changed, 136 insertions(+), 163 deletions(-) delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java index fba358ea51..fd649f0924 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java @@ -20,8 +20,12 @@ import static org.junit.Assert.fail; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.OggTestData; +import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.EOFException; import java.io.IOException; import java.util.Random; import org.junit.Test; @@ -31,6 +35,8 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class DefaultOggSeekerTest { + private final Random random = new Random(0); + @Test public void testSetupWithUnsetEndPositionFails() { try { @@ -55,6 +61,95 @@ public final class DefaultOggSeekerTest { } } + @Test + public void testSkipToNextPage() throws Exception { + FakeExtractorInput extractorInput = + OggTestData.createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(4000, random), + new byte[] {'O', 'g', 'g', 'S'}, + TestUtil.buildTestData(4000, random)), + false); + skipToNextPage(extractorInput); + assertThat(extractorInput.getPosition()).isEqualTo(4000); + } + + @Test + public void testSkipToNextPageOverlap() throws Exception { + FakeExtractorInput extractorInput = + OggTestData.createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(2046, random), + new byte[] {'O', 'g', 'g', 'S'}, + TestUtil.buildTestData(4000, random)), + false); + skipToNextPage(extractorInput); + assertThat(extractorInput.getPosition()).isEqualTo(2046); + } + + @Test + public void testSkipToNextPageInputShorterThanPeekLength() throws Exception { + FakeExtractorInput extractorInput = + OggTestData.createInput( + TestUtil.joinByteArrays(new byte[] {'x', 'O', 'g', 'g', 'S'}), false); + skipToNextPage(extractorInput); + assertThat(extractorInput.getPosition()).isEqualTo(1); + } + + @Test + public void testSkipToNextPageNoMatch() throws Exception { + FakeExtractorInput extractorInput = + OggTestData.createInput(new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false); + try { + skipToNextPage(extractorInput); + fail(); + } catch (EOFException e) { + // expected + } + } + + @Test + public void testReadGranuleOfLastPage() throws IOException, InterruptedException { + FakeExtractorInput input = + OggTestData.createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(100, random), + OggTestData.buildOggHeader(0x00, 20000, 66, 3), + TestUtil.createByteArray(254, 254, 254), // laces + TestUtil.buildTestData(3 * 254, random), + OggTestData.buildOggHeader(0x00, 40000, 67, 3), + TestUtil.createByteArray(254, 254, 254), // laces + TestUtil.buildTestData(3 * 254, random), + OggTestData.buildOggHeader(0x05, 60000, 68, 3), + TestUtil.createByteArray(254, 254, 254), // laces + TestUtil.buildTestData(3 * 254, random)), + false); + assertReadGranuleOfLastPage(input, 60000); + } + + @Test + public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException { + FakeExtractorInput input = OggTestData.createInput(TestUtil.buildTestData(100, random), false); + try { + assertReadGranuleOfLastPage(input, 60000); + fail(); + } catch (EOFException e) { + // Ignored. + } + } + + @Test + public void testReadGranuleOfLastPageWithUnboundedLength() + throws IOException, InterruptedException { + FakeExtractorInput input = OggTestData.createInput(new byte[0], true); + try { + assertReadGranuleOfLastPage(input, 60000); + fail(); + } catch (IllegalArgumentException e) { + // Ignored. + } + } + private void testSeeking(Random random) throws IOException, InterruptedException { OggTestFile testFile = OggTestFile.generate(random, 1000); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(testFile.data).build(); @@ -130,7 +225,47 @@ public final class DefaultOggSeekerTest { } } - private long seekTo( + private static void skipToNextPage(ExtractorInput extractorInput) + throws IOException, InterruptedException { + DefaultOggSeeker oggSeeker = + new DefaultOggSeeker( + /* streamReader= */ new FlacReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ extractorInput.getLength(), + /* firstPayloadPageSize= */ 1, + /* firstPayloadPageGranulePosition= */ 2, + /* firstPayloadPageIsLastPage= */ false); + while (true) { + try { + oggSeeker.skipToNextPage(extractorInput); + break; + } catch (FakeExtractorInput.SimulatedIOException e) { + /* ignored */ + } + } + } + + private static void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) + throws IOException, InterruptedException { + DefaultOggSeeker oggSeeker = + new DefaultOggSeeker( + /* streamReader= */ new FlacReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ input.getLength(), + /* firstPayloadPageSize= */ 1, + /* firstPayloadPageGranulePosition= */ 2, + /* firstPayloadPageIsLastPage= */ false); + while (true) { + try { + assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected); + break; + } catch (FakeExtractorInput.SimulatedIOException e) { + // Ignored. + } + } + } + + private static long seekTo( FakeExtractorInput input, DefaultOggSeeker oggSeeker, long targetGranule, int initialPosition) throws IOException, InterruptedException { long nextSeekPosition = initialPosition; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java deleted file mode 100644 index 2521602228..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * 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.extractor.ogg; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.testutil.FakeExtractorInput; -import com.google.android.exoplayer2.testutil.OggTestData; -import com.google.android.exoplayer2.testutil.TestUtil; -import java.io.EOFException; -import java.io.IOException; -import java.util.Random; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit test for {@link DefaultOggSeeker} utility methods. */ -@RunWith(AndroidJUnit4.class) -public final class DefaultOggSeekerUtilMethodsTest { - - private final Random random = new Random(0); - - @Test - public void testSkipToNextPage() throws Exception { - FakeExtractorInput extractorInput = OggTestData.createInput( - TestUtil.joinByteArrays( - TestUtil.buildTestData(4000, random), - new byte[] {'O', 'g', 'g', 'S'}, - TestUtil.buildTestData(4000, random) - ), false); - skipToNextPage(extractorInput); - assertThat(extractorInput.getPosition()).isEqualTo(4000); - } - - @Test - public void testSkipToNextPageOverlap() throws Exception { - FakeExtractorInput extractorInput = OggTestData.createInput( - TestUtil.joinByteArrays( - TestUtil.buildTestData(2046, random), - new byte[] {'O', 'g', 'g', 'S'}, - TestUtil.buildTestData(4000, random) - ), false); - skipToNextPage(extractorInput); - assertThat(extractorInput.getPosition()).isEqualTo(2046); - } - - @Test - public void testSkipToNextPageInputShorterThanPeekLength() throws Exception { - FakeExtractorInput extractorInput = OggTestData.createInput( - TestUtil.joinByteArrays( - new byte[] {'x', 'O', 'g', 'g', 'S'} - ), false); - skipToNextPage(extractorInput); - assertThat(extractorInput.getPosition()).isEqualTo(1); - } - - @Test - public void testSkipToNextPageNoMatch() throws Exception { - FakeExtractorInput extractorInput = OggTestData.createInput( - new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false); - try { - skipToNextPage(extractorInput); - fail(); - } catch (EOFException e) { - // expected - } - } - - private static void skipToNextPage(ExtractorInput extractorInput) - throws IOException, InterruptedException { - DefaultOggSeeker oggSeeker = - new DefaultOggSeeker( - /* streamReader= */ new FlacReader(), - /* payloadStartPosition= */ 0, - /* payloadEndPosition= */ extractorInput.getLength(), - /* firstPayloadPageSize= */ 1, - /* firstPayloadPageGranulePosition= */ 2, - /* firstPayloadPageIsLastPage= */ false); - while (true) { - try { - oggSeeker.skipToNextPage(extractorInput); - break; - } catch (FakeExtractorInput.SimulatedIOException e) { /* ignored */ } - } - } - - @Test - public void testReadGranuleOfLastPage() throws IOException, InterruptedException { - FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays( - TestUtil.buildTestData(100, random), - OggTestData.buildOggHeader(0x00, 20000, 66, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random), - OggTestData.buildOggHeader(0x00, 40000, 67, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random), - OggTestData.buildOggHeader(0x05, 60000, 68, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random) - ), false); - assertReadGranuleOfLastPage(input, 60000); - } - - @Test - public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException { - FakeExtractorInput input = OggTestData.createInput(TestUtil.buildTestData(100, random), false); - try { - assertReadGranuleOfLastPage(input, 60000); - fail(); - } catch (EOFException e) { - // Ignored. - } - } - - @Test - public void testReadGranuleOfLastPageWithUnboundedLength() - throws IOException, InterruptedException { - FakeExtractorInput input = OggTestData.createInput(new byte[0], true); - try { - assertReadGranuleOfLastPage(input, 60000); - fail(); - } catch (IllegalArgumentException e) { - // Ignored. - } - } - - private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) - throws IOException, InterruptedException { - DefaultOggSeeker oggSeeker = - new DefaultOggSeeker( - /* streamReader= */ new FlacReader(), - /* payloadStartPosition= */ 0, - /* payloadEndPosition= */ input.getLength(), - /* firstPayloadPageSize= */ 1, - /* firstPayloadPageGranulePosition= */ 2, - /* firstPayloadPageIsLastPage= */ false); - while (true) { - try { - assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected); - break; - } catch (FakeExtractorInput.SimulatedIOException e) { - // Ignored. - } - } - } - -} From f179feb292fca717cf7a1af02b8c31a308810ed2 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 16:49:17 +0100 Subject: [PATCH 1528/1556] Constraint seek targetGranule within bounds + simplify tests PiperOrigin-RevId: 261328701 --- .../extractor/ogg/DefaultOggSeeker.java | 8 +-- .../extractor/ogg/DefaultOggSeekerTest.java | 26 ++------- .../exoplayer2/extractor/ogg/OggTestFile.java | 58 +++++++++++-------- 3 files changed, 43 insertions(+), 49 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index 064bd5732d..51ab94ba0e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -29,8 +29,8 @@ import java.io.IOException; /** Seeks in an Ogg stream. */ /* package */ final class DefaultOggSeeker implements OggSeeker { - @VisibleForTesting public static final int MATCH_RANGE = 72000; - @VisibleForTesting public static final int MATCH_BYTE_RANGE = 100000; + private static final int MATCH_RANGE = 72000; + private static final int MATCH_BYTE_RANGE = 100000; private static final int DEFAULT_OFFSET = 30000; private static final int STATE_SEEK_TO_END = 0; @@ -127,7 +127,7 @@ import java.io.IOException; @Override public void startSeek(long targetGranule) { - this.targetGranule = targetGranule; + this.targetGranule = Util.constrainValue(targetGranule, 0, totalGranules - 1); state = STATE_SEEK; start = payloadStartPosition; end = payloadEndPosition; @@ -201,7 +201,7 @@ import java.io.IOException; private void skipToPageOfTargetGranule(ExtractorInput input) throws IOException, InterruptedException { pageHeader.populate(input, /* quiet= */ false); - while (pageHeader.granulePosition < targetGranule) { + while (pageHeader.granulePosition <= targetGranule) { input.skipFully(pageHeader.headerSize + pageHeader.bodySize); start = input.getPosition(); startGranule = pageHeader.granulePosition; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java index fd649f0924..8ba0be26a0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java @@ -160,7 +160,7 @@ public final class DefaultOggSeekerTest { /* payloadStartPosition= */ 0, /* payloadEndPosition= */ testFile.data.length, /* firstPayloadPageSize= */ testFile.firstPayloadPageSize, - /* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranulePosition, + /* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranuleCount, /* firstPayloadPageIsLastPage= */ false); OggPageHeader pageHeader = new OggPageHeader(); @@ -183,28 +183,12 @@ public final class DefaultOggSeekerTest { assertThat(input.getPosition()).isEqualTo(0); // Test last granule. - granule = seekTo(input, oggSeeker, testFile.lastGranule, 0); - long position = testFile.data.length; - // TODO: Simplify this. - assertThat( - (testFile.lastGranule > granule && position > input.getPosition()) - || (testFile.lastGranule == granule && position == input.getPosition())) - .isTrue(); - - // Test exact granule. - input.setPosition(testFile.data.length / 2); - oggSeeker.skipToNextPage(input); - assertThat(pageHeader.populate(input, true)).isTrue(); - position = input.getPosition() + pageHeader.headerSize + pageHeader.bodySize; - granule = seekTo(input, oggSeeker, pageHeader.granulePosition, 0); - // TODO: Simplify this. - assertThat( - (pageHeader.granulePosition > granule && position > input.getPosition()) - || (pageHeader.granulePosition == granule && position == input.getPosition())) - .isTrue(); + granule = seekTo(input, oggSeeker, testFile.granuleCount - 1, 0); + assertThat(granule).isEqualTo(testFile.granuleCount - testFile.lastPayloadPageGranuleCount); + assertThat(input.getPosition()).isEqualTo(testFile.data.length - testFile.lastPayloadPageSize); for (int i = 0; i < 100; i += 1) { - long targetGranule = (long) (random.nextDouble() * testFile.lastGranule); + long targetGranule = random.nextInt(testFile.granuleCount); int initialPosition = random.nextInt(testFile.data.length); granule = seekTo(input, oggSeeker, targetGranule, initialPosition); long currentPosition = input.getPosition(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java index e5512dda36..38e4332b16 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java @@ -30,35 +30,39 @@ import java.util.Random; private static final int MAX_GRANULES_IN_PAGE = 100000; public final byte[] data; - public final long lastGranule; - public final int packetCount; + public final int granuleCount; public final int pageCount; public final int firstPayloadPageSize; - public final long firstPayloadPageGranulePosition; + public final int firstPayloadPageGranuleCount; + public final int lastPayloadPageSize; + public final int lastPayloadPageGranuleCount; private OggTestFile( byte[] data, - long lastGranule, - int packetCount, + int granuleCount, int pageCount, int firstPayloadPageSize, - long firstPayloadPageGranulePosition) { + int firstPayloadPageGranuleCount, + int lastPayloadPageSize, + int lastPayloadPageGranuleCount) { this.data = data; - this.lastGranule = lastGranule; - this.packetCount = packetCount; + this.granuleCount = granuleCount; this.pageCount = pageCount; this.firstPayloadPageSize = firstPayloadPageSize; - this.firstPayloadPageGranulePosition = firstPayloadPageGranulePosition; + this.firstPayloadPageGranuleCount = firstPayloadPageGranuleCount; + this.lastPayloadPageSize = lastPayloadPageSize; + this.lastPayloadPageGranuleCount = lastPayloadPageGranuleCount; } public static OggTestFile generate(Random random, int pageCount) { ArrayList fileData = new ArrayList<>(); int fileSize = 0; - long granule = 0; - int packetLength = -1; - int packetCount = 0; + int granuleCount = 0; int firstPayloadPageSize = 0; - long firstPayloadPageGranulePosition = 0; + int firstPayloadPageGranuleCount = 0; + int lastPageloadPageSize = 0; + int lastPayloadPageGranuleCount = 0; + int packetLength = -1; for (int i = 0; i < pageCount; i++) { int headerType = 0x00; @@ -71,17 +75,17 @@ import java.util.Random; if (i == pageCount - 1) { headerType |= 4; } - granule += random.nextInt(MAX_GRANULES_IN_PAGE - 1) + 1; + int pageGranuleCount = random.nextInt(MAX_GRANULES_IN_PAGE - 1) + 1; int pageSegmentCount = random.nextInt(MAX_SEGMENT_COUNT); - byte[] header = OggTestData.buildOggHeader(headerType, granule, 0, pageSegmentCount); + granuleCount += pageGranuleCount; + byte[] header = OggTestData.buildOggHeader(headerType, granuleCount, 0, pageSegmentCount); fileData.add(header); - fileSize += header.length; + int pageSize = header.length; byte[] laces = new byte[pageSegmentCount]; int bodySize = 0; for (int j = 0; j < pageSegmentCount; j++) { if (packetLength < 0) { - packetCount++; if (i < pageCount - 1) { packetLength = random.nextInt(MAX_PACKET_LENGTH); } else { @@ -96,14 +100,19 @@ import java.util.Random; packetLength -= 255; } fileData.add(laces); - fileSize += laces.length; + pageSize += laces.length; byte[] payload = TestUtil.buildTestData(bodySize, random); fileData.add(payload); - fileSize += payload.length; + pageSize += payload.length; + + fileSize += pageSize; if (i == 0) { - firstPayloadPageSize = header.length + bodySize; - firstPayloadPageGranulePosition = granule; + firstPayloadPageSize = pageSize; + firstPayloadPageGranuleCount = pageGranuleCount; + } else if (i == pageCount - 1) { + lastPageloadPageSize = pageSize; + lastPayloadPageGranuleCount = pageGranuleCount; } } @@ -115,11 +124,12 @@ import java.util.Random; } return new OggTestFile( file, - granule, - packetCount, + granuleCount, pageCount, firstPayloadPageSize, - firstPayloadPageGranulePosition); + firstPayloadPageGranuleCount, + lastPageloadPageSize, + lastPayloadPageGranuleCount); } public int findPreviousPageStart(long position) { From 818ef62fb0626f878089a5c50edfe0da49e32a32 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 18:02:21 +0100 Subject: [PATCH 1529/1556] Remove obsolete workaround PiperOrigin-RevId: 261340526 --- build.gradle | 8 -------- 1 file changed, 8 deletions(-) diff --git a/build.gradle b/build.gradle index bc538ead68..1d0b459bf5 100644 --- a/build.gradle +++ b/build.gradle @@ -21,14 +21,6 @@ buildscript { classpath 'com.novoda:bintray-release:0.9' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0' } - // Workaround for the following test coverage issue. Remove when fixed: - // https://code.google.com/p/android/issues/detail?id=226070 - configurations.all { - resolutionStrategy { - force 'org.jacoco:org.jacoco.report:0.7.4.201502262128' - force 'org.jacoco:org.jacoco.core:0.7.4.201502262128' - } - } } allprojects { repositories { From 6f014749b3e96d573a081837e92a66805b992de1 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 18:05:17 +0100 Subject: [PATCH 1530/1556] Upgrade dependency versions PiperOrigin-RevId: 261341256 --- extensions/cast/build.gradle | 2 +- extensions/cronet/build.gradle | 2 +- extensions/workmanager/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index e067789bc4..83e994c5e1 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -31,7 +31,7 @@ android { } dependencies { - api 'com.google.android.gms:play-services-cast-framework:16.2.0' + api 'com.google.android.gms:play-services-cast-framework:17.0.0' implementation 'androidx.annotation:annotation:1.0.2' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 0808ad6c44..7561857dca 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -31,7 +31,7 @@ android { } dependencies { - api 'org.chromium.net:cronet-embedded:73.3683.76' + api 'org.chromium.net:cronet-embedded:75.3770.101' implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.0.2' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle index 9065855a3f..ea7564316f 100644 --- a/extensions/workmanager/build.gradle +++ b/extensions/workmanager/build.gradle @@ -34,7 +34,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.work:work-runtime:2.0.1' + implementation 'androidx.work:work-runtime:2.1.0' } ext { From fb0481c520b5b7eefa51abe01625a16b5009013d Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 19:03:03 +0100 Subject: [PATCH 1531/1556] Bump annotations dependency + update release notes PiperOrigin-RevId: 261353271 --- RELEASENOTES.md | 20 ++++++++++---------- demos/gvr/build.gradle | 2 +- demos/ima/build.gradle | 2 +- demos/main/build.gradle | 2 +- extensions/cast/build.gradle | 2 +- extensions/cronet/build.gradle | 2 +- extensions/ffmpeg/build.gradle | 2 +- extensions/flac/build.gradle | 2 +- extensions/gvr/build.gradle | 2 +- extensions/ima/build.gradle | 2 +- extensions/leanback/build.gradle | 2 +- extensions/okhttp/build.gradle | 2 +- extensions/opus/build.gradle | 2 +- extensions/rtmp/build.gradle | 2 +- extensions/vp9/build.gradle | 2 +- library/core/build.gradle | 2 +- library/dash/build.gradle | 2 +- library/hls/build.gradle | 2 +- library/smoothstreaming/build.gradle | 2 +- library/ui/build.gradle | 2 +- playbacktests/build.gradle | 2 +- testutils/build.gradle | 2 +- testutils_robolectric/build.gradle | 2 +- 23 files changed, 32 insertions(+), 32 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 89acdfb9a4..bfdf9e733e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,12 +27,6 @@ over other selection parameters. * Remove `AnalyticsCollector.Factory`. Instances can be created directly and the `Player` set later using `AnalyticsCollector.setPlayer`. -* Calculate correct duration for clipped WAV streams - ([#6241](https://github.com/google/ExoPlayer/issues/6241)). -* Fix Flac and ALAC playback on some LG devices - ([#5938](https://github.com/google/ExoPlayer/issues/5938)). -* MP3: use CBR header bitrate, not calculated bitrate. This reverts a change - from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)). ### 2.10.4 ### @@ -41,6 +35,14 @@ ExoPlayer library classes. * Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language tags instead of 3-letter ISO 639-2 language tags. +* Ensure the `SilenceMediaSource` position is in range + ([#6229](https://github.com/google/ExoPlayer/issues/6229)). +* WAV: Calculate correct duration for clipped streams + ([#6241](https://github.com/google/ExoPlayer/issues/6241)). +* MP3: Use CBR header bitrate, not calculated bitrate. This reverts a change + from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)). +* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata + ([#5527](https://github.com/google/ExoPlayer/issues/5527)). * Fix issue where initial seek positions get ignored when playing a preroll ad ([#6201](https://github.com/google/ExoPlayer/issues/6201)). * Fix issue where invalid language tags were normalized to "und" instead of @@ -48,10 +50,8 @@ ([#6153](https://github.com/google/ExoPlayer/issues/6153)). * Fix `DataSchemeDataSource` re-opening and range requests ([#6192](https://github.com/google/ExoPlayer/issues/6192)). -* Ensure the `SilenceMediaSource` position is in range - ([#6229](https://github.com/google/ExoPlayer/issues/6229)). -* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata - ([#5527](https://github.com/google/ExoPlayer/issues/5527)). +* Fix Flac and ALAC playback on some LG devices + ([#5938](https://github.com/google/ExoPlayer/issues/5938)). ### 2.10.3 ### diff --git a/demos/gvr/build.gradle b/demos/gvr/build.gradle index 457af80a8d..37d8fbbb99 100644 --- a/demos/gvr/build.gradle +++ b/demos/gvr/build.gradle @@ -53,7 +53,7 @@ dependencies { implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'extension-gvr') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' } apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index 33161b4121..124555d9b5 100644 --- a/demos/ima/build.gradle +++ b/demos/ima/build.gradle @@ -53,7 +53,7 @@ dependencies { implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'extension-ima') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' } apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/main/build.gradle b/demos/main/build.gradle index 0bce1d4b82..f58389d9d4 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -62,7 +62,7 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.viewpager:viewpager:1.0.0' implementation 'androidx.fragment:fragment:1.0.0' implementation 'com.google.android.material:material:1.0.0' diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 83e994c5e1..68a7494a3f 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -32,7 +32,7 @@ android { dependencies { api 'com.google.android.gms:play-services-cast-framework:17.0.0' - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 7561857dca..f7cc707fb4 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -33,7 +33,7 @@ android { dependencies { api 'org.chromium.net:cronet-embedded:75.3770.101' implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'testutils-robolectric') diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index ffecdcd16f..15952b1860 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -38,7 +38,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 10b244cb39..c67de27697 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -39,7 +39,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index 50acd6c040..1031d6f4b7 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -33,7 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' api 'com.google.vr:sdk-base:1.190.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion } diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 2df9448d08..0ef9f281c9 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -34,7 +34,7 @@ android { dependencies { api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2' implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle index c6f5a216ce..ecaa78e25b 100644 --- a/extensions/leanback/build.gradle +++ b/extensions/leanback/build.gradle @@ -32,7 +32,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.leanback:leanback:1.0.0' } diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index db2e073c8a..68bd422185 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -33,7 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion api 'com.squareup.okhttp3:okhttp:3.12.1' } diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index 0795079c6b..28f7b05465 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -39,7 +39,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index ca734c3657..b74be659ee 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -33,7 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'net.butterflytv.utils:rtmp-client:3.0.1' - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index fe1f220af6..51b2677368 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -39,7 +39,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion diff --git a/library/core/build.gradle b/library/core/build.gradle index ecb81c4450..93126d9830 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -58,7 +58,7 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion diff --git a/library/dash/build.gradle b/library/dash/build.gradle index f6981a2220..9f5775d478 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 8e9696af70..82e09ab72c 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -39,7 +39,7 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation project(modulePrefix + 'library-core') testImplementation project(modulePrefix + 'testutils-robolectric') diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index a2e81fb304..fa67ea1d01 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 6384bf920f..5182dfccf5 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.media:media:1.0.1' - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index dd5cfa64a7..5865d3c36d 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -34,7 +34,7 @@ android { dependencies { androidTestImplementation 'androidx.test:rules:' + androidXTestVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion - androidTestImplementation 'androidx.annotation:annotation:1.0.2' + androidTestImplementation 'androidx.annotation:annotation:1.1.0' androidTestImplementation project(modulePrefix + 'library-core') androidTestImplementation project(modulePrefix + 'library-dash') androidTestImplementation project(modulePrefix + 'library-hls') diff --git a/testutils/build.gradle b/testutils/build.gradle index 36465f5d5f..afd2a146af 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -41,7 +41,7 @@ dependencies { api 'org.mockito:mockito-core:' + mockitoVersion api 'androidx.test.ext:junit:' + androidXTestVersion api 'com.google.truth:truth:' + truthVersion - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' implementation project(modulePrefix + 'library-core') implementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion annotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion diff --git a/testutils_robolectric/build.gradle b/testutils_robolectric/build.gradle index 78fa5dbd87..a098178429 100644 --- a/testutils_robolectric/build.gradle +++ b/testutils_robolectric/build.gradle @@ -41,6 +41,6 @@ dependencies { api 'org.robolectric:robolectric:' + robolectricVersion api project(modulePrefix + 'testutils') implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' annotationProcessor 'com.google.auto.service:auto-service:' + autoServiceVersion } From d6e74bc19b05a3c4cc1d6c7684a4692aea5ce1e3 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 5 Aug 2019 10:21:55 +0100 Subject: [PATCH 1532/1556] Ensure position reset keep window sequence number. We currently keep the sequence number if we don't reset the position. However, the sequence number should be kept if we don't reset the state. Otherwise re-prepare with position reset is counted as new playback although it's still the same. PiperOrigin-RevId: 261644924 --- .../android/exoplayer2/ExoPlayerImpl.java | 2 +- .../exoplayer2/ExoPlayerImplInternal.java | 6 +-- .../android/exoplayer2/PlaybackInfo.java | 19 ++++++--- .../android/exoplayer2/ExoPlayerTest.java | 42 +++++++++++++++++++ 4 files changed, 60 insertions(+), 9 deletions(-) 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 3eed66402d..e99429d3b2 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 @@ -647,7 +647,7 @@ import java.util.concurrent.CopyOnWriteArrayList; resetPosition = resetPosition || resetState; MediaPeriodId mediaPeriodId = resetPosition - ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window) + ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period) : playbackInfo.periodId; long startPositionUs = resetPosition ? 0 : playbackInfo.positionUs; long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs; 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 4fe8da92c2..6ab0838e26 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 @@ -646,7 +646,7 @@ import java.util.concurrent.atomic.AtomicBoolean; if (resolvedSeekPosition == null) { // The seek position was valid for the timeline that it was performed into, but the // timeline has changed or is not ready and a suitable seek position could not be resolved. - periodId = playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window); + periodId = playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period); periodPositionUs = C.TIME_UNSET; contentPositionUs = C.TIME_UNSET; seekPositionAdjusted = true; @@ -884,7 +884,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - queue.clear(/* keepFrontPeriodUid= */ !resetPosition); + queue.clear(/* keepFrontPeriodUid= */ !resetState); setIsLoading(false); if (resetState) { queue.setTimeline(Timeline.EMPTY); @@ -896,7 +896,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } MediaPeriodId mediaPeriodId = resetPosition - ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window) + ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period) : playbackInfo.periodId; // Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored. long startPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.positionUs; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index 669f41ca13..e9b99acd77 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -150,17 +150,26 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; * * @param shuffleModeEnabled Whether shuffle mode is enabled. * @param window A writable {@link Timeline.Window}. + * @param period A writable {@link Timeline.Period}. * @return A dummy media period id for the first-to-be-played period of the current timeline. */ public MediaPeriodId getDummyFirstMediaPeriodId( - boolean shuffleModeEnabled, Timeline.Window window) { + boolean shuffleModeEnabled, Timeline.Window window, Timeline.Period period) { if (timeline.isEmpty()) { return DUMMY_MEDIA_PERIOD_ID; } - int firstPeriodIndex = - timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) - .firstPeriodIndex; - return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex)); + int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); + int firstPeriodIndex = timeline.getWindow(firstWindowIndex, window).firstPeriodIndex; + int currentPeriodIndex = timeline.getIndexOfPeriod(periodId.periodUid); + long windowSequenceNumber = C.INDEX_UNSET; + if (currentPeriodIndex != C.INDEX_UNSET) { + int currentWindowIndex = timeline.getPeriod(currentPeriodIndex, period).windowIndex; + if (firstWindowIndex == currentWindowIndex) { + // Keep window sequence number if the new position is still in the same window. + windowSequenceNumber = periodId.windowSequenceNumber; + } + } + return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex), windowSequenceNumber); } /** 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 d5b0b2c667..f924dfb34c 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 @@ -28,6 +28,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; @@ -59,6 +60,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; @@ -1434,6 +1436,46 @@ public final class ExoPlayerTest { assertThat(windowIndexHolder[2]).isEqualTo(1); } + @Test + public void playbackErrorAndReprepareWithPositionResetKeepsWindowSequenceNumber() + throws Exception { + FakeMediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("playbackErrorWithResetKeepsWindowSequenceNumber") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) + .waitForPlaybackState(Player.STATE_IDLE) + .prepareSource(mediaSource, /* resetPosition= */ true, /* resetState= */ false) + .waitForPlaybackState(Player.STATE_READY) + .play() + .build(); + HashSet reportedWindowSequenceNumbers = new HashSet<>(); + AnalyticsListener listener = + new AnalyticsListener() { + @Override + public void onPlayerStateChanged( + EventTime eventTime, boolean playWhenReady, int playbackState) { + if (eventTime.mediaPeriodId != null) { + reportedWindowSequenceNumbers.add(eventTime.mediaPeriodId.windowSequenceNumber); + } + } + }; + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .setAnalyticsListener(listener) + .build(context); + try { + testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + fail(); + } catch (ExoPlaybackException e) { + // Expected exception. + } + assertThat(reportedWindowSequenceNumbers).hasSize(1); + } + @Test public void testPlaybackErrorTwiceStillKeepsTimeline() throws Exception { final Timeline timeline = new FakeTimeline(/* windowCount= */ 1); From 790deb71db4a344fe65e06e8f17e9c31744c13a7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 5 Aug 2019 10:43:50 +0100 Subject: [PATCH 1533/1556] Check if controller is used when performing click directly. Issue:#6260 PiperOrigin-RevId: 261647858 --- RELEASENOTES.md | 3 +++ .../java/com/google/android/exoplayer2/ui/PlayerView.java | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bfdf9e733e..03b45fc945 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,6 +27,9 @@ over other selection parameters. * Remove `AnalyticsCollector.Factory`. Instances can be created directly and the `Player` set later using `AnalyticsCollector.setPlayer`. +* Fix issue when calling `performClick` on `PlayerView` without + `PlayerControlView` + ([#6260](https://github.com/google/ExoPlayer/issues/6260)). ### 2.10.4 ### 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 d97a53bc4d..ec6e94e042 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 @@ -1156,6 +1156,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider // Internal methods. private boolean toggleControllerVisibility() { + if (!useController || player == null) { + return false; + } if (!controller.isVisible()) { maybeShowController(true); } else if (controllerHideOnTouch) { @@ -1491,9 +1494,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Override public boolean onSingleTapUp(MotionEvent e) { - if (!useController || player == null) { - return false; - } return toggleControllerVisibility(); } } From c9b73cba8c06cb3928c077bdb47bfa4873b92684 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 5 Aug 2019 11:13:07 +0100 Subject: [PATCH 1534/1556] Update release notes PiperOrigin-RevId: 261651655 --- RELEASENOTES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 03b45fc945..4ffd8c4e5d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,9 +27,6 @@ over other selection parameters. * Remove `AnalyticsCollector.Factory`. Instances can be created directly and the `Player` set later using `AnalyticsCollector.setPlayer`. -* Fix issue when calling `performClick` on `PlayerView` without - `PlayerControlView` - ([#6260](https://github.com/google/ExoPlayer/issues/6260)). ### 2.10.4 ### @@ -55,6 +52,9 @@ ([#6192](https://github.com/google/ExoPlayer/issues/6192)). * Fix Flac and ALAC playback on some LG devices ([#5938](https://github.com/google/ExoPlayer/issues/5938)). +* Fix issue when calling `performClick` on `PlayerView` without + `PlayerControlView` + ([#6260](https://github.com/google/ExoPlayer/issues/6260)). ### 2.10.3 ### From b6441a02f5eb8a8377e90db1b392ae2ad9b372e1 Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Mon, 5 Aug 2019 16:40:20 +0100 Subject: [PATCH 1535/1556] Introduce common output buffer class for video decoders PiperOrigin-RevId: 261693054 --- .../exoplayer2/ext/vp9/VpxOutputBuffer.java | 113 +--------------- .../video/VideoDecoderOutputBuffer.java | 125 ++++++++++++++++++ 2 files changed, 128 insertions(+), 110 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java index de411089ab..7177cde12e 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java @@ -15,39 +15,12 @@ */ package com.google.android.exoplayer2.ext.vp9; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.decoder.OutputBuffer; -import com.google.android.exoplayer2.video.ColorInfo; -import java.nio.ByteBuffer; +import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer; -/** Output buffer containing video frame data, populated by {@link VpxDecoder}. */ -public final class VpxOutputBuffer extends OutputBuffer { - - public static final int COLORSPACE_UNKNOWN = 0; - public static final int COLORSPACE_BT601 = 1; - public static final int COLORSPACE_BT709 = 2; - public static final int COLORSPACE_BT2020 = 3; +/** Video output buffer, populated by {@link VpxDecoder}. */ +public final class VpxOutputBuffer extends VideoDecoderOutputBuffer { private final VpxDecoder owner; - /** Decoder private data. */ - public int decoderPrivate; - - /** Output mode. */ - @C.VideoOutputMode public int mode; - /** - * RGB buffer for RGB mode. - */ - public ByteBuffer data; - public int width; - public int height; - public ColorInfo colorInfo; - - /** - * YUV planes for YUV mode. - */ - public ByteBuffer[] yuvPlanes; - public int[] yuvStrides; - public int colorspace; public VpxOutputBuffer(VpxDecoder owner) { this.owner = owner; @@ -58,84 +31,4 @@ public final class VpxOutputBuffer extends OutputBuffer { owner.releaseOutputBuffer(this); } - /** - * Initializes the buffer. - * - * @param timeUs The presentation timestamp for the buffer, in microseconds. - * @param mode The output mode. One of {@link C#VIDEO_OUTPUT_MODE_NONE}, {@link - * C#VIDEO_OUTPUT_MODE_YUV} and {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV}. - */ - public void init(long timeUs, @C.VideoOutputMode int mode) { - this.timeUs = timeUs; - this.mode = mode; - } - - /** - * Resizes the buffer based on the given stride. Called via JNI after decoding completes. - * - * @return Whether the buffer was resized successfully. - */ - public boolean initForYuvFrame(int width, int height, int yStride, int uvStride, int colorspace) { - this.width = width; - this.height = height; - this.colorspace = colorspace; - int uvHeight = (int) (((long) height + 1) / 2); - if (!isSafeToMultiply(yStride, height) || !isSafeToMultiply(uvStride, uvHeight)) { - return false; - } - int yLength = yStride * height; - int uvLength = uvStride * uvHeight; - int minimumYuvSize = yLength + (uvLength * 2); - if (!isSafeToMultiply(uvLength, 2) || minimumYuvSize < yLength) { - return false; - } - initData(minimumYuvSize); - - if (yuvPlanes == null) { - yuvPlanes = new ByteBuffer[3]; - } - // Rewrapping has to be done on every frame since the stride might have changed. - yuvPlanes[0] = data.slice(); - yuvPlanes[0].limit(yLength); - data.position(yLength); - yuvPlanes[1] = data.slice(); - yuvPlanes[1].limit(uvLength); - data.position(yLength + uvLength); - yuvPlanes[2] = data.slice(); - yuvPlanes[2].limit(uvLength); - if (yuvStrides == null) { - yuvStrides = new int[3]; - } - yuvStrides[0] = yStride; - yuvStrides[1] = uvStride; - yuvStrides[2] = uvStride; - return true; - } - - /** - * Configures the buffer for the given frame dimensions when passing actual frame data via {@link - * #decoderPrivate}. Called via JNI after decoding completes. - */ - public void initForPrivateFrame(int width, int height) { - this.width = width; - this.height = height; - } - - private void initData(int size) { - if (data == null || data.capacity() < size) { - data = ByteBuffer.allocateDirect(size); - } else { - data.position(0); - data.limit(size); - } - } - - /** - * Ensures that the result of multiplying individual numbers can fit into the size limit of an - * integer. - */ - private boolean isSafeToMultiply(int a, int b) { - return a >= 0 && b >= 0 && !(b > 0 && a >= Integer.MAX_VALUE / b); - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java new file mode 100644 index 0000000000..af0844defb --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2019 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.video; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.decoder.OutputBuffer; +import java.nio.ByteBuffer; + +/** Video decoder output buffer containing video frame data. */ +public abstract class VideoDecoderOutputBuffer extends OutputBuffer { + + public static final int COLORSPACE_UNKNOWN = 0; + public static final int COLORSPACE_BT601 = 1; + public static final int COLORSPACE_BT709 = 2; + public static final int COLORSPACE_BT2020 = 3; + + /** Decoder private data. */ + public int decoderPrivate; + + /** Output mode. */ + @C.VideoOutputMode public int mode; + /** RGB buffer for RGB mode. */ + public ByteBuffer data; + + public int width; + public int height; + public ColorInfo colorInfo; + + /** YUV planes for YUV mode. */ + public ByteBuffer[] yuvPlanes; + + public int[] yuvStrides; + public int colorspace; + + /** + * Initializes the buffer. + * + * @param timeUs The presentation timestamp for the buffer, in microseconds. + * @param mode The output mode. One of {@link C#VIDEO_OUTPUT_MODE_NONE}, {@link + * C#VIDEO_OUTPUT_MODE_YUV} and {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV}. + */ + public void init(long timeUs, @C.VideoOutputMode int mode) { + this.timeUs = timeUs; + this.mode = mode; + } + + /** + * Resizes the buffer based on the given stride. Called via JNI after decoding completes. + * + * @return Whether the buffer was resized successfully. + */ + public boolean initForYuvFrame(int width, int height, int yStride, int uvStride, int colorspace) { + this.width = width; + this.height = height; + this.colorspace = colorspace; + int uvHeight = (int) (((long) height + 1) / 2); + if (!isSafeToMultiply(yStride, height) || !isSafeToMultiply(uvStride, uvHeight)) { + return false; + } + int yLength = yStride * height; + int uvLength = uvStride * uvHeight; + int minimumYuvSize = yLength + (uvLength * 2); + if (!isSafeToMultiply(uvLength, 2) || minimumYuvSize < yLength) { + return false; + } + + // Initialize data. + if (data == null || data.capacity() < minimumYuvSize) { + data = ByteBuffer.allocateDirect(minimumYuvSize); + } else { + data.position(0); + data.limit(minimumYuvSize); + } + + if (yuvPlanes == null) { + yuvPlanes = new ByteBuffer[3]; + } + // Rewrapping has to be done on every frame since the stride might have changed. + yuvPlanes[0] = data.slice(); + yuvPlanes[0].limit(yLength); + data.position(yLength); + yuvPlanes[1] = data.slice(); + yuvPlanes[1].limit(uvLength); + data.position(yLength + uvLength); + yuvPlanes[2] = data.slice(); + yuvPlanes[2].limit(uvLength); + if (yuvStrides == null) { + yuvStrides = new int[3]; + } + yuvStrides[0] = yStride; + yuvStrides[1] = uvStride; + yuvStrides[2] = uvStride; + return true; + } + + /** + * Configures the buffer for the given frame dimensions when passing actual frame data via {@link + * #decoderPrivate}. Called via JNI after decoding completes. + */ + public void initForPrivateFrame(int width, int height) { + this.width = width; + this.height = height; + } + + /** + * Ensures that the result of multiplying individual numbers can fit into the size limit of an + * integer. + */ + private static boolean isSafeToMultiply(int a, int b) { + return a >= 0 && b >= 0 && !(b > 0 && a >= Integer.MAX_VALUE / b); + } +} From 17a9030e1d02220efbf32e8c65ca925f630e66ad Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 5 Aug 2019 16:57:44 +0100 Subject: [PATCH 1536/1556] Update stale TrackSelections in chunk sources when keeping the streams. If we keep streams in chunk sources after selecting new tracks, we also keep a reference to a stale disabled TrackSelection object. Fix this by updating the TrackSelection object when keeping the stream. The static part of the selection (i.e. the subset of selected tracks) stays the same in all cases. Issue:#6256 PiperOrigin-RevId: 261696082 --- RELEASENOTES.md | 3 +++ .../android/exoplayer2/source/MediaPeriod.java | 5 ++++- .../exoplayer2/source/dash/DashChunkSource.java | 7 +++++++ .../exoplayer2/source/dash/DashMediaPeriod.java | 16 +++++++++++++--- .../source/dash/DefaultDashChunkSource.java | 7 ++++++- .../exoplayer2/source/hls/HlsChunkSource.java | 10 ++++------ .../source/hls/HlsSampleStreamWrapper.java | 17 ++++++++++------- .../smoothstreaming/DefaultSsChunkSource.java | 7 ++++++- .../source/smoothstreaming/SsChunkSource.java | 7 +++++++ .../source/smoothstreaming/SsMediaPeriod.java | 1 + 10 files changed, 61 insertions(+), 19 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4ffd8c4e5d..06cfde8d6c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -55,6 +55,9 @@ * Fix issue when calling `performClick` on `PlayerView` without `PlayerControlView` ([#6260](https://github.com/google/ExoPlayer/issues/6260)). +* Fix issue where playback speeds are not used in adaptive track selections + after manual selection changes for other renderers + ([#6256](https://github.com/google/ExoPlayer/issues/6256)). ### 2.10.3 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index f076eae32c..847c87b077 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -106,13 +106,16 @@ public interface MediaPeriod extends SequenceableLoader { * Performs a track selection. * *

          The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags} - * indicating whether the existing {@code SampleStream} can be retained for each selection, and + * indicating whether the existing {@link SampleStream} can be retained for each selection, and * the existing {@code stream}s themselves. The call will update {@code streams} to reflect the * provided selections, clearing, setting and replacing entries as required. If an existing sample * stream is retained but with the requirement that the consuming renderer be reset, then the * corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set * if a new sample stream is created. * + *

          Note that previously received {@link TrackSelection TrackSelections} are no longer valid and + * references need to be replaced even if the corresponding {@link SampleStream} is kept. + * *

          This method is only called after the period has been prepared. * * @param selections The renderer track selections. diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java index 40d4e468bd..f7edf62182 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java @@ -69,4 +69,11 @@ public interface DashChunkSource extends ChunkSource { * @param newManifest The new manifest. */ void updateManifest(DashManifest newManifest, int periodIndex); + + /** + * Updates the track selection. + * + * @param trackSelection The new track selection instance. Must be equivalent to the previous one. + */ + void updateTrackSelection(TrackSelection trackSelection); } 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 b34b677d45..5daa1a8fd5 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 @@ -406,17 +406,27 @@ import java.util.regex.Pattern; int[] streamIndexToTrackGroupIndex) { // Create newly selected primary and event streams. for (int i = 0; i < selections.length; i++) { - if (streams[i] == null && selections[i] != null) { + TrackSelection selection = selections[i]; + if (selection == null) { + continue; + } + if (streams[i] == null) { + // Create new stream for selection. streamResetFlags[i] = true; int trackGroupIndex = streamIndexToTrackGroupIndex[i]; TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) { - streams[i] = buildSampleStream(trackGroupInfo, selections[i], positionUs); + streams[i] = buildSampleStream(trackGroupInfo, selection, positionUs); } else if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_MANIFEST_EVENTS) { EventStream eventStream = eventStreams.get(trackGroupInfo.eventStreamGroupIndex); - Format format = selections[i].getTrackGroup().getFormat(0); + Format format = selection.getTrackGroup().getFormat(0); streams[i] = new EventSampleStream(eventStream, format, manifest.dynamic); } + } else if (streams[i] instanceof ChunkSampleStream) { + // Update selection in existing stream. + @SuppressWarnings("unchecked") + ChunkSampleStream stream = (ChunkSampleStream) streams[i]; + stream.getChunkSource().updateTrackSelection(selection); } } // Create newly selected embedded streams from the corresponding primary stream. Note that this 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 2de81a2535..bcf0a1766a 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 @@ -111,7 +111,6 @@ public class DefaultDashChunkSource implements DashChunkSource { private final LoaderErrorThrower manifestLoaderErrorThrower; private final int[] adaptationSetIndices; - private final TrackSelection trackSelection; private final int trackType; private final DataSource dataSource; private final long elapsedRealtimeOffsetMs; @@ -120,6 +119,7 @@ public class DefaultDashChunkSource implements DashChunkSource { protected final RepresentationHolder[] representationHolders; + private TrackSelection trackSelection; private DashManifest manifest; private int periodIndex; private IOException fatalError; @@ -222,6 +222,11 @@ public class DefaultDashChunkSource implements DashChunkSource { } } + @Override + public void updateTrackSelection(TrackSelection trackSelection) { + this.trackSelection = trackSelection; + } + @Override public void maybeThrowError() throws IOException { if (fatalError != null) { 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 261c9b531c..ee5a5f0809 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 @@ -183,17 +183,15 @@ import java.util.Map; } /** - * Selects tracks for use. + * Sets the current track selection. * - * @param trackSelection The track selection. + * @param trackSelection The {@link TrackSelection}. */ - public void selectTracks(TrackSelection trackSelection) { + public void setTrackSelection(TrackSelection trackSelection) { this.trackSelection = trackSelection; } - /** - * Returns the current track selection. - */ + /** Returns the current {@link TrackSelection}. */ public TrackSelection getTrackSelection() { return trackSelection; } 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 c8c1b8f566..ff725ec6f7 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 @@ -306,14 +306,17 @@ import java.util.Set; TrackSelection primaryTrackSelection = oldPrimaryTrackSelection; // Select new tracks. for (int i = 0; i < selections.length; i++) { - if (streams[i] == null && selections[i] != null) { + TrackSelection selection = selections[i]; + if (selection == null) { + continue; + } + int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); + if (trackGroupIndex == primaryTrackGroupIndex) { + primaryTrackSelection = selection; + chunkSource.setTrackSelection(selection); + } + if (streams[i] == null) { enabledTrackGroupCount++; - TrackSelection selection = selections[i]; - int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); - if (trackGroupIndex == primaryTrackGroupIndex) { - primaryTrackSelection = selection; - chunkSource.selectTracks(selection); - } streams[i] = new HlsSampleStream(this, trackGroupIndex); streamResetFlags[i] = true; if (trackGroupToSampleQueueIndex != null) { diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index 59e18195e2..22dfb04f13 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -74,10 +74,10 @@ public class DefaultSsChunkSource implements SsChunkSource { private final LoaderErrorThrower manifestLoaderErrorThrower; private final int streamElementIndex; - private final TrackSelection trackSelection; private final ChunkExtractorWrapper[] extractorWrappers; private final DataSource dataSource; + private TrackSelection trackSelection; private SsManifest manifest; private int currentManifestChunkOffset; @@ -155,6 +155,11 @@ public class DefaultSsChunkSource implements SsChunkSource { manifest = newManifest; } + @Override + public void updateTrackSelection(TrackSelection trackSelection) { + this.trackSelection = trackSelection; + } + // ChunkSource implementation. @Override diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java index b763a484b8..111393140e 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java @@ -55,4 +55,11 @@ public interface SsChunkSource extends ChunkSource { * @param newManifest The new manifest. */ void updateManifest(SsManifest newManifest); + + /** + * Updates the track selection. + * + * @param trackSelection The new track selection instance. Must be equivalent to the previous one. + */ + void updateTrackSelection(TrackSelection trackSelection); } 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 286ec82ed6..d103358d37 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 @@ -131,6 +131,7 @@ import java.util.List; stream.release(); streams[i] = null; } else { + stream.getChunkSource().updateTrackSelection(selections[i]); sampleStreamsList.add(stream); } } From 346f8e670af19b33e4575fa200f90008c7360cba Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 5 Aug 2019 17:21:31 +0100 Subject: [PATCH 1537/1556] Turn on non-null-by-default for most extensions. PiperOrigin-RevId: 261700729 --- .../exoplayer2/ext/cast/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/ext/cronet/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/ext/ffmpeg/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/ext/flac/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/ext/ima/ImaAdsLoader.java | 2 +- .../exoplayer2/ext/ima/package-info.java | 19 +++++++++++++++++++ .../ext/jobdispatcher/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/ext/leanback/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/ext/okhttp/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/ext/opus/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/ext/rtmp/package-info.java | 19 +++++++++++++++++++ .../ext/workmanager/package-info.java | 19 +++++++++++++++++++ 12 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/package-info.java create mode 100644 extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/package-info.java create mode 100644 extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/package-info.java create mode 100644 extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/package-info.java create mode 100644 extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/package-info.java create mode 100644 extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java create mode 100644 extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/package-info.java create mode 100644 extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/package-info.java create mode 100644 extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/package-info.java create mode 100644 extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/package-info.java create mode 100644 extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/package-info.java diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/package-info.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/package-info.java new file mode 100644 index 0000000000..07055905a6 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.ext.cast; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/package-info.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/package-info.java new file mode 100644 index 0000000000..ec0cf8df05 --- /dev/null +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.ext.cronet; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/package-info.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/package-info.java new file mode 100644 index 0000000000..a9fedb19cb --- /dev/null +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.ext.ffmpeg; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/package-info.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/package-info.java new file mode 100644 index 0000000000..ef6da7e3c6 --- /dev/null +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.ext.flac; + +import com.google.android.exoplayer2.util.NonNullApi; 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 249271dc61..e37f192c97 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 @@ -426,7 +426,7 @@ public final class ImaAdsLoader * @deprecated Use {@link ImaAdsLoader.Builder}. */ @Deprecated - public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) { + public ImaAdsLoader(Context context, Uri adTagUri, @Nullable ImaSdkSettings imaSdkSettings) { this( context, adTagUri, diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/package-info.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/package-info.java new file mode 100644 index 0000000000..9a382eb18f --- /dev/null +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.ext.ima; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java new file mode 100644 index 0000000000..a66904b505 --- /dev/null +++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.ext.jobdispatcher; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/package-info.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/package-info.java new file mode 100644 index 0000000000..79c544fc0f --- /dev/null +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.ext.leanback; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/package-info.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/package-info.java new file mode 100644 index 0000000000..54eb4d5967 --- /dev/null +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.ext.okhttp; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/package-info.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/package-info.java new file mode 100644 index 0000000000..0848937fdc --- /dev/null +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.ext.opus; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/package-info.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/package-info.java new file mode 100644 index 0000000000..cb16630bd3 --- /dev/null +++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.ext.rtmp; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/package-info.java b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/package-info.java new file mode 100644 index 0000000000..7e0e244231 --- /dev/null +++ b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.ext.workmanager; + +import com.google.android.exoplayer2.util.NonNullApi; From 591bd6e46a0c10138fe0ea466c42378c030fbc89 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 6 Aug 2019 10:34:15 +0100 Subject: [PATCH 1538/1556] Fix UI module API nullability annotations and make non-null-by-default. PiperOrigin-RevId: 261872025 --- .../exoplayer2/ui/PlayerControlView.java | 10 ++++---- .../android/exoplayer2/ui/PlayerView.java | 23 ++++++++++++------- .../android/exoplayer2/ui/package-info.java | 19 +++++++++++++++ .../exoplayer2/ui/spherical/package-info.java | 19 +++++++++++++++ 4 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 library/ui/src/main/java/com/google/android/exoplayer2/ui/package-info.java create mode 100644 library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/package-info.java 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 e408035e98..3a194e091a 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 @@ -285,7 +285,7 @@ public class PlayerControlView extends FrameLayout { } public PlayerControlView(Context context, @Nullable AttributeSet attrs) { - this(context, attrs, 0); + this(context, attrs, /* defStyleAttr= */ 0); } public PlayerControlView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { @@ -494,9 +494,10 @@ public class PlayerControlView extends FrameLayout { /** * Sets the {@link VisibilityListener}. * - * @param listener The listener to be notified about visibility changes. + * @param listener The listener to be notified about visibility changes, or null to remove the + * current listener. */ - public void setVisibilityListener(VisibilityListener listener) { + public void setVisibilityListener(@Nullable VisibilityListener listener) { this.visibilityListener = listener; } @@ -512,7 +513,8 @@ public class PlayerControlView extends FrameLayout { /** * Sets the {@link PlaybackPreparer}. * - * @param playbackPreparer The {@link PlaybackPreparer}. + * @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback + * preparer. */ public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) { this.playbackPreparer = playbackPreparer; 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 ec6e94e042..0d66922cab 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 @@ -308,14 +308,14 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider private static final int PICTURE_TYPE_NOT_SET = -1; public PlayerView(Context context) { - this(context, null); + this(context, /* attrs= */ null); } - public PlayerView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + public PlayerView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, /* defStyleAttr= */ 0); } - public PlayerView(Context context, AttributeSet attrs, int defStyleAttr) { + public PlayerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); if (isInEditMode()) { @@ -505,6 +505,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } /** Returns the player currently set on this view, or null if no player is set. */ + @Nullable public Player getPlayer() { return player; } @@ -904,9 +905,11 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider /** * Set the {@link PlayerControlView.VisibilityListener}. * - * @param listener The listener to be notified about visibility changes. + * @param listener The listener to be notified about visibility changes, or null to remove the + * current listener. */ - public void setControllerVisibilityListener(PlayerControlView.VisibilityListener listener) { + public void setControllerVisibilityListener( + @Nullable PlayerControlView.VisibilityListener listener) { Assertions.checkState(controller != null); controller.setVisibilityListener(listener); } @@ -914,7 +917,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider /** * Sets the {@link PlaybackPreparer}. * - * @param playbackPreparer The {@link PlaybackPreparer}. + * @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback + * preparer. */ public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) { Assertions.checkState(controller != null); @@ -1006,7 +1010,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @param listener The listener to be notified about aspect ratios changes of the video content or * the content frame. */ - public void setAspectRatioListener(AspectRatioFrameLayout.AspectRatioListener listener) { + public void setAspectRatioListener( + @Nullable AspectRatioFrameLayout.AspectRatioListener listener) { Assertions.checkState(contentFrame != null); contentFrame.setAspectRatioListener(listener); } @@ -1025,6 +1030,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @return The {@link SurfaceView}, {@link TextureView}, {@link SphericalSurfaceView} or {@code * null}. */ + @Nullable public View getVideoSurfaceView() { return surfaceView; } @@ -1047,6 +1053,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @return The {@link SubtitleView}, or {@code null} if the layout has been customized and the * subtitle view is not present. */ + @Nullable public SubtitleView getSubtitleView() { return subtitleView; } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/package-info.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/package-info.java new file mode 100644 index 0000000000..85903f4659 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.ui; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/package-info.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/package-info.java new file mode 100644 index 0000000000..bbbffc7a44 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.ui.spherical; + +import com.google.android.exoplayer2.util.NonNullApi; From 4603188165e465cdec9d5e971c649cb398c50c43 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 6 Aug 2019 11:16:02 +0100 Subject: [PATCH 1539/1556] Add inband emsg-v1 support to FragmentedMp4Extractor This also decouples EventMessageEncoder's serialization schema from the emesg spec (it happens to still match the emsg-v0 spec, but this is no longer required). PiperOrigin-RevId: 261877918 --- .../java/com/google/android/exoplayer2/C.java | 7 +- .../extractor/mp4/FragmentedMp4Extractor.java | 92 ++++++++++++------- .../metadata/emsg/EventMessageDecoder.java | 8 +- 3 files changed, 66 insertions(+), 41 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 9ed5cb7e36..daa6124df6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -71,9 +71,10 @@ public final class C { /** Represents an unset or unknown percentage. */ public static final int PERCENTAGE_UNSET = -1; - /** - * The number of microseconds in one second. - */ + /** The number of milliseconds in one second. */ + public static final long MILLIS_PER_SECOND = 1000L; + + /** The number of microseconds in one second. */ public static final long MICROS_PER_SECOND = 1000000L; /** 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 392d4d9179..7ff7912729 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 @@ -35,6 +35,8 @@ import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; import com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom; +import com.google.android.exoplayer2.metadata.emsg.EventMessage; +import com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder; import com.google.android.exoplayer2.text.cea.CeaUtil; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; @@ -141,6 +143,8 @@ public class FragmentedMp4Extractor implements Extractor { // Adjusts sample timestamps. @Nullable private final TimestampAdjuster timestampAdjuster; + private final EventMessageEncoder eventMessageEncoder; + // Parser state. private final ParsableByteArray atomHeader; private final ArrayDeque containerAtoms; @@ -254,6 +258,7 @@ public class FragmentedMp4Extractor implements Extractor { this.sideloadedDrmInitData = sideloadedDrmInitData; this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats); this.additionalEmsgTrackOutput = additionalEmsgTrackOutput; + eventMessageEncoder = new EventMessageEncoder(); atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalPrefix = new ParsableByteArray(5); @@ -591,39 +596,71 @@ public class FragmentedMp4Extractor implements Extractor { } } - /** - * Parses an emsg atom (defined in 23009-1). - */ + /** Handles an emsg atom (defined in 23009-1). */ private void onEmsgLeafAtomRead(ParsableByteArray atom) { if (emsgTrackOutputs == null || emsgTrackOutputs.length == 0) { return; } + atom.setPosition(Atom.HEADER_SIZE); + int fullAtom = atom.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + String schemeIdUri; + String value; + long timescale; + long presentationTimeDeltaUs = C.TIME_UNSET; // Only set if version == 0 + long sampleTimeUs = C.TIME_UNSET; + long durationMs; + long id; + switch (version) { + case 0: + schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString()); + value = Assertions.checkNotNull(atom.readNullTerminatedString()); + timescale = atom.readUnsignedInt(); + presentationTimeDeltaUs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale); + if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) { + sampleTimeUs = segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs; + } + durationMs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); + id = atom.readUnsignedInt(); + break; + case 1: + timescale = atom.readUnsignedInt(); + sampleTimeUs = + Util.scaleLargeTimestamp(atom.readUnsignedLongToLong(), C.MICROS_PER_SECOND, timescale); + durationMs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); + id = atom.readUnsignedInt(); + schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString()); + value = Assertions.checkNotNull(atom.readNullTerminatedString()); + break; + default: + Log.w(TAG, "Skipping unsupported emsg version: " + version); + return; + } - atom.setPosition(Atom.FULL_HEADER_SIZE); - int sampleSize = atom.bytesLeft(); - atom.readNullTerminatedString(); // schemeIdUri - atom.readNullTerminatedString(); // value - long timescale = atom.readUnsignedInt(); - long presentationTimeDeltaUs = - Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale); - - // The presentation_time_delta is accounted for by adjusting the sample timestamp, so we zero it - // in the sample data before writing it to the track outputs. - int position = atom.getPosition(); - atom.data[position - 4] = 0; - atom.data[position - 3] = 0; - atom.data[position - 2] = 0; - atom.data[position - 1] = 0; + byte[] messageData = new byte[atom.bytesLeft()]; + atom.readBytes(messageData, /*offset=*/ 0, atom.bytesLeft()); + EventMessage eventMessage = new EventMessage(schemeIdUri, value, durationMs, id, messageData); + ParsableByteArray encodedEventMessage = + new ParsableByteArray(eventMessageEncoder.encode(eventMessage)); + int sampleSize = encodedEventMessage.bytesLeft(); // Output the sample data. for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { - atom.setPosition(Atom.FULL_HEADER_SIZE); - emsgTrackOutput.sampleData(atom, sampleSize); + encodedEventMessage.setPosition(0); + emsgTrackOutput.sampleData(encodedEventMessage, sampleSize); } - // Output the sample metadata. - if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) { - long sampleTimeUs = segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs; + // Output the sample metadata. This is made a little complicated because emsg-v0 atoms + // have presentation time *delta* while v1 atoms have absolute presentation time. + if (sampleTimeUs == C.TIME_UNSET) { + // We need the first sample timestamp in the segment before we can output the metadata. + pendingMetadataSampleInfos.addLast( + new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize)); + pendingMetadataSampleBytes += sampleSize; + } else { if (timestampAdjuster != null) { sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); } @@ -631,17 +668,10 @@ public class FragmentedMp4Extractor implements Extractor { emsgTrackOutput.sampleMetadata( sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, /* offset= */ 0, null); } - } else { - // We need the first sample timestamp in the segment before we can output the metadata. - pendingMetadataSampleInfos.addLast( - new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize)); - pendingMetadataSampleBytes += sampleSize; } } - /** - * Parses a trex atom (defined in 14496-12). - */ + /** Parses a trex atom (defined in 14496-12). */ private static Pair parseTrex(ParsableByteArray trex) { trex.setPosition(Atom.FULL_HEADER_SIZE); int trackId = trex.readInt(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index 33d79917eb..87d0491a7b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -25,13 +25,7 @@ import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.util.Arrays; -/** - * Decodes Event Message (emsg) atoms, as defined in ISO/IEC 23009-1:2014, Section 5.10.3.3. - * - *

          Atom data should be provided to the decoder without the full atom header (i.e. starting from - * the first byte of the scheme_id_uri field). It is expected that the presentation_time_delta field - * should be 0, having already been accounted for by adjusting the sample timestamp. - */ +/** Decodes data encoded by {@link EventMessageEncoder}. */ public final class EventMessageDecoder implements MetadataDecoder { private static final String TAG = "EventMessageDecoder"; From 3b9288b805e2bf26d4d4ce2c131075afd007f3ae Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 6 Aug 2019 11:16:40 +0100 Subject: [PATCH 1540/1556] Migrate literal usages of 1000 to (new) C.MILLIS_PER_SECOND This only covers calls to scaleLargeTimestamp() PiperOrigin-RevId: 261878019 --- .../exoplayer2/extractor/mp4/FragmentedMp4Extractor.java | 9 ++++++--- .../exoplayer2/metadata/emsg/EventMessageDecoder.java | 4 +++- .../source/dash/manifest/DashManifestParser.java | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) 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 7ff7912729..5eaa5d5d31 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 @@ -965,7 +965,9 @@ public class FragmentedMp4Extractor implements Extractor { // duration == 0). Other uses of edit lists are uncommon and unsupported. if (track.editListDurations != null && track.editListDurations.length == 1 && track.editListDurations[0] == 0) { - edtsOffset = Util.scaleLargeTimestamp(track.editListMediaTimes[0], 1000, track.timescale); + edtsOffset = + Util.scaleLargeTimestamp( + track.editListMediaTimes[0], C.MILLIS_PER_SECOND, track.timescale); } int[] sampleSizeTable = fragment.sampleSizeTable; @@ -993,12 +995,13 @@ public class FragmentedMp4Extractor implements Extractor { // here, because unsigned integers will still be parsed correctly (unless their top bit is // set, which is never true in practice because sample offsets are always small). int sampleOffset = trun.readInt(); - sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000L) / timescale); + sampleCompositionTimeOffsetTable[i] = + (int) ((sampleOffset * C.MILLIS_PER_SECOND) / timescale); } else { sampleCompositionTimeOffsetTable[i] = 0; } sampleDecodingTimeTable[i] = - Util.scaleLargeTimestamp(cumulativeTime, 1000, timescale) - edtsOffset; + Util.scaleLargeTimestamp(cumulativeTime, C.MILLIS_PER_SECOND, timescale) - edtsOffset; sampleSizeTable[i] = sampleSize; sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0 && (!workaroundEveryVideoFrameIsSyncFrame || i == 0); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index 87d0491a7b..a49bf956b3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.metadata.emsg; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; @@ -46,7 +47,8 @@ public final class EventMessageDecoder implements MetadataDecoder { // timestamp and zeroing the field in the sample data. Log a warning if the field is non-zero. Log.w(TAG, "Ignoring non-zero presentation_time_delta: " + presentationTimeDelta); } - long durationMs = Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), 1000, timescale); + long durationMs = + Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); long id = emsgData.readUnsignedInt(); byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size); return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData)); 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 8bf142f397..1419f8198c 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 @@ -899,7 +899,7 @@ public class DashManifestParser extends DefaultHandler long id = parseLong(xpp, "id", 0); long duration = parseLong(xpp, "duration", C.TIME_UNSET); long presentationTime = parseLong(xpp, "presentationTime", 0); - long durationMs = Util.scaleLargeTimestamp(duration, 1000, timescale); + long durationMs = Util.scaleLargeTimestamp(duration, C.MILLIS_PER_SECOND, timescale); long presentationTimesUs = Util.scaleLargeTimestamp(presentationTime, C.MICROS_PER_SECOND, timescale); String messageData = parseString(xpp, "messageData", null); From b0330edc0b83105a034b95cb9dca096a2ed1e1c6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 6 Aug 2019 12:46:39 +0100 Subject: [PATCH 1541/1556] Fix some Android Studio nullness warning created by new @NonNullApi. PiperOrigin-RevId: 261888086 --- .../mediasession/MediaSessionConnector.java | 28 ++++++++++--------- .../ext/mediasession/TimelineQueueEditor.java | 2 +- .../exoplayer2/offline/ActionFile.java | 6 ++-- .../offline/ActionFileUpgradeUtil.java | 2 +- .../exoplayer2/offline/DownloadHelper.java | 4 +-- .../exoplayer2/offline/DownloadManager.java | 16 +++++------ .../exoplayer2/offline/DownloadService.java | 8 +++--- .../exoplayer2/offline/SegmentDownloader.java | 2 +- 8 files changed, 35 insertions(+), 33 deletions(-) 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 be085ae30b..cb1788f2fc 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 @@ -558,7 +558,7 @@ public final class MediaSessionConnector { * * @param queueNavigator The queue navigator. */ - public void setQueueNavigator(QueueNavigator queueNavigator) { + public void setQueueNavigator(@Nullable QueueNavigator queueNavigator) { if (this.queueNavigator != queueNavigator) { unregisterCommandReceiver(this.queueNavigator); this.queueNavigator = queueNavigator; @@ -571,7 +571,7 @@ public final class MediaSessionConnector { * * @param queueEditor The queue editor. */ - public void setQueueEditor(QueueEditor queueEditor) { + public void setQueueEditor(@Nullable QueueEditor queueEditor) { if (this.queueEditor != queueEditor) { unregisterCommandReceiver(this.queueEditor); this.queueEditor = queueEditor; @@ -673,7 +673,7 @@ public final class MediaSessionConnector { mediaMetadataProvider != null && player != null ? mediaMetadataProvider.getMetadata(player) : METADATA_EMPTY; - mediaSession.setMetadata(metadata != null ? metadata : METADATA_EMPTY); + mediaSession.setMetadata(metadata); } /** @@ -684,7 +684,7 @@ public final class MediaSessionConnector { */ public final void invalidateMediaSessionPlaybackState() { PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); - Player player = this.player; + @Nullable Player player = this.player; if (player == null) { builder.setActions(buildPrepareActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0); mediaSession.setPlaybackState(builder.build()); @@ -693,6 +693,7 @@ public final class MediaSessionConnector { Map currentActions = new HashMap<>(); for (CustomActionProvider customActionProvider : customActionProviders) { + @Nullable PlaybackStateCompat.CustomAction customAction = customActionProvider.getCustomAction(player); if (customAction != null) { currentActions.put(customAction.getAction(), customActionProvider); @@ -703,6 +704,7 @@ public final class MediaSessionConnector { int playbackState = player.getPlaybackState(); Bundle extras = new Bundle(); + @Nullable ExoPlaybackException playbackError = playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null; boolean reportError = playbackError != null || customError != null; @@ -949,10 +951,10 @@ public final class MediaSessionConnector { MediaSessionCompat.QueueItem queueItem = queue.get(i); if (queueItem.getQueueId() == activeQueueItemId) { MediaDescriptionCompat description = queueItem.getDescription(); - Bundle extras = description.getExtras(); + @Nullable Bundle extras = description.getExtras(); if (extras != null) { for (String key : extras.keySet()) { - Object value = extras.get(key); + @Nullable Object value = extras.get(key); if (value instanceof String) { builder.putString(metadataExtrasPrefix + key, (String) value); } else if (value instanceof CharSequence) { @@ -968,37 +970,37 @@ public final class MediaSessionConnector { } } } - CharSequence title = description.getTitle(); + @Nullable CharSequence title = description.getTitle(); if (title != null) { String titleString = String.valueOf(title); builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, titleString); builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, titleString); } - CharSequence subtitle = description.getSubtitle(); + @Nullable CharSequence subtitle = description.getSubtitle(); if (subtitle != null) { builder.putString( MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, String.valueOf(subtitle)); } - CharSequence displayDescription = description.getDescription(); + @Nullable CharSequence displayDescription = description.getDescription(); if (displayDescription != null) { builder.putString( MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, String.valueOf(displayDescription)); } - Bitmap iconBitmap = description.getIconBitmap(); + @Nullable Bitmap iconBitmap = description.getIconBitmap(); if (iconBitmap != null) { builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, iconBitmap); } - Uri iconUri = description.getIconUri(); + @Nullable Uri iconUri = description.getIconUri(); if (iconUri != null) { builder.putString( MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, String.valueOf(iconUri)); } - String mediaId = description.getMediaId(); + @Nullable String mediaId = description.getMediaId(); if (mediaId != null) { builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId); } - Uri mediaUri = description.getMediaUri(); + @Nullable Uri mediaUri = description.getMediaUri(); if (mediaUri != null) { builder.putString( MediaMetadataCompat.METADATA_KEY_MEDIA_URI, String.valueOf(mediaUri)); diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java index d076404bb4..d72f6ffddc 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java @@ -166,7 +166,7 @@ public final class TimelineQueueEditor @Override public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) { - MediaSource mediaSource = sourceFactory.createMediaSource(description); + @Nullable MediaSource mediaSource = sourceFactory.createMediaSource(description); if (mediaSource != null) { queueDataAdapter.add(index, description); queueMediaSource.addMediaSource(index, mediaSource); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java index a053185435..c69908c746 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java @@ -68,7 +68,7 @@ import java.util.List; if (!exists()) { return new DownloadRequest[0]; } - InputStream inputStream = null; + @Nullable InputStream inputStream = null; try { inputStream = atomicFile.openRead(); DataInputStream dataInputStream = new DataInputStream(inputStream); @@ -99,7 +99,7 @@ import java.util.List; boolean isRemoveAction = input.readBoolean(); int dataLength = input.readInt(); - byte[] data; + @Nullable byte[] data; if (dataLength != 0) { data = new byte[dataLength]; input.readFully(data); @@ -123,7 +123,7 @@ import java.util.List; && (DownloadRequest.TYPE_DASH.equals(type) || DownloadRequest.TYPE_HLS.equals(type) || DownloadRequest.TYPE_SS.equals(type)); - String customCacheKey = null; + @Nullable String customCacheKey = null; if (!isLegacySegmented) { customCacheKey = input.readBoolean() ? input.readUTF() : null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java index baf47772ab..9ecce6e150 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java @@ -97,7 +97,7 @@ public final class ActionFileUpgradeUtil { boolean addNewDownloadAsCompleted, long nowMs) throws IOException { - Download download = downloadIndex.getDownload(request.id); + @Nullable Download download = downloadIndex.getDownload(request.id); if (download != null) { download = DownloadManager.mergeRequest(download, request, download.stopReason, nowMs); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 6952413129..54360f8f6b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -390,7 +390,7 @@ public final class DownloadHelper { */ public static MediaSource createMediaSource( DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) { - Constructor constructor; + @Nullable Constructor constructor; switch (downloadRequest.type) { case DownloadRequest.TYPE_DASH: constructor = DASH_FACTORY_CONSTRUCTOR; @@ -808,7 +808,7 @@ public final class DownloadHelper { new MediaPeriodId(mediaPreparer.timeline.getUidOfPeriod(periodIndex)), mediaPreparer.timeline); for (int i = 0; i < trackSelectorResult.length; i++) { - TrackSelection newSelection = trackSelectorResult.selections.get(i); + @Nullable TrackSelection newSelection = trackSelectorResult.selections.get(i); if (newSelection == null) { continue; } 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 ec5ff81d97..c3cf0bdc24 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 @@ -731,7 +731,7 @@ public final class DownloadManager { Log.e(TAG, "Failed to set manual stop reason", e); } } else { - Download download = getDownload(id, /* loadFromIndex= */ false); + @Nullable Download download = getDownload(id, /* loadFromIndex= */ false); if (download != null) { setStopReason(download, stopReason); } else { @@ -779,7 +779,7 @@ public final class DownloadManager { } private void addDownload(DownloadRequest request, int stopReason) { - Download download = getDownload(request.id, /* loadFromIndex= */ true); + @Nullable Download download = getDownload(request.id, /* loadFromIndex= */ true); long nowMs = System.currentTimeMillis(); if (download != null) { putDownload(mergeRequest(download, request, stopReason, nowMs)); @@ -798,7 +798,7 @@ public final class DownloadManager { } private void removeDownload(String id) { - Download download = getDownload(id, /* loadFromIndex= */ true); + @Nullable Download download = getDownload(id, /* loadFromIndex= */ true); if (download == null) { Log.e(TAG, "Failed to remove nonexistent download: " + id); return; @@ -860,7 +860,7 @@ public final class DownloadManager { int accumulatingDownloadTaskCount = 0; for (int i = 0; i < downloads.size(); i++) { Download download = downloads.get(i); - Task activeTask = activeTasks.get(download.request.id); + @Nullable Task activeTask = activeTasks.get(download.request.id); switch (download.state) { case STATE_STOPPED: syncStoppedDownload(activeTask); @@ -999,7 +999,7 @@ public final class DownloadManager { return; } - Throwable finalError = task.finalError; + @Nullable Throwable finalError = task.finalError; if (finalError != null) { Log.e(TAG, "Task failed: " + task.request + ", " + isRemove, finalError); } @@ -1176,7 +1176,7 @@ public final class DownloadManager { private final boolean isRemove; private final int minRetryCount; - private volatile InternalHandler internalHandler; + @Nullable private volatile InternalHandler internalHandler; private volatile boolean isCanceled; @Nullable private Throwable finalError; @@ -1246,7 +1246,7 @@ public final class DownloadManager { } catch (Throwable e) { finalError = e; } - Handler internalHandler = this.internalHandler; + @Nullable Handler internalHandler = this.internalHandler; if (internalHandler != null) { internalHandler.obtainMessage(MSG_TASK_STOPPED, this).sendToTarget(); } @@ -1258,7 +1258,7 @@ public final class DownloadManager { downloadProgress.percentDownloaded = percentDownloaded; if (contentLength != this.contentLength) { this.contentLength = contentLength; - Handler internalHandler = this.internalHandler; + @Nullable Handler internalHandler = this.internalHandler; if (internalHandler != null) { internalHandler.obtainMessage(MSG_CONTENT_LENGTH_CHANGED, this).sendToTarget(); } 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 107cedd728..db10517b67 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 @@ -592,8 +592,8 @@ public abstract class DownloadService extends Service { public int onStartCommand(Intent intent, int flags, int startId) { lastStartId = startId; taskRemoved = false; - String intentAction = null; - String contentId = null; + @Nullable String intentAction = null; + @Nullable String contentId = null; if (intent != null) { intentAction = intent.getAction(); startedInForeground |= @@ -611,7 +611,7 @@ public abstract class DownloadService extends Service { // Do nothing. break; case ACTION_ADD_DOWNLOAD: - DownloadRequest downloadRequest = intent.getParcelableExtra(KEY_DOWNLOAD_REQUEST); + @Nullable DownloadRequest downloadRequest = intent.getParcelableExtra(KEY_DOWNLOAD_REQUEST); if (downloadRequest == null) { Log.e(TAG, "Ignored ADD_DOWNLOAD: Missing " + KEY_DOWNLOAD_REQUEST + " extra"); } else { @@ -644,7 +644,7 @@ public abstract class DownloadService extends Service { } break; case ACTION_SET_REQUIREMENTS: - Requirements requirements = intent.getParcelableExtra(KEY_REQUIREMENTS); + @Nullable Requirements requirements = intent.getParcelableExtra(KEY_REQUIREMENTS); if (requirements == null) { Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra"); } else { 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 1643812ece..5326220452 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 @@ -138,7 +138,7 @@ public abstract class SegmentDownloader> impleme Collections.sort(segments); // Download the segments. - ProgressNotifier progressNotifier = null; + @Nullable ProgressNotifier progressNotifier = null; if (progressListener != null) { progressNotifier = new ProgressNotifier( From a9b93d7ec2b30450c1f15dbf1b1abefd0c8cb705 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 6 Aug 2019 15:34:36 +0100 Subject: [PATCH 1542/1556] Fix some remaining extension API nullability issues. PiperOrigin-RevId: 261910303 --- .../exoplayer2/ext/cast/CastUtils.java | 3 +- .../ext/ffmpeg/FfmpegAudioRenderer.java | 6 ++-- .../ext/flac/LibflacAudioRenderer.java | 6 ++-- .../ext/opus/LibopusAudioRenderer.java | 6 ++-- .../exoplayer2/ext/opus/OpusDecoder.java | 31 ++++++++++++++----- .../audio/SimpleDecoderAudioRenderer.java | 9 +++--- .../audio/SimpleDecoderAudioRendererTest.java | 29 +++++++++-------- 7 files changed, 56 insertions(+), 34 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java index d1660c3306..1dc25576a0 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.cast; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.gms.cast.CastStatusCodes; @@ -33,7 +34,7 @@ import com.google.android.gms.cast.MediaTrack; * @param mediaInfo The media info to get the duration from. * @return The duration in microseconds, or {@link C#TIME_UNSET} if unknown or not applicable. */ - public static long getStreamDurationUs(MediaInfo mediaInfo) { + public static long getStreamDurationUs(@Nullable MediaInfo mediaInfo) { if (mediaInfo == null) { return C.TIME_UNSET; } diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index c5d80aa32b..39d1ee4094 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -92,8 +92,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected int supportsFormatInternal(DrmSessionManager drmSessionManager, - Format format) { + protected int supportsFormatInternal( + @Nullable DrmSessionManager drmSessionManager, Format format) { Assertions.checkNotNull(format.sampleMimeType); if (!FfmpegLibrary.isAvailable()) { return FORMAT_UNSUPPORTED_TYPE; @@ -113,7 +113,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) + protected FfmpegDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) throws FfmpegDecoderException { int initialInputBufferSize = format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java index 376d0fd75e..d833c47d14 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -51,8 +51,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected int supportsFormatInternal(DrmSessionManager drmSessionManager, - Format format) { + protected int supportsFormatInternal( + @Nullable DrmSessionManager drmSessionManager, Format format) { if (!FlacLibrary.isAvailable() || !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; @@ -66,7 +66,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) + protected FlacDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) throws FlacDecoderException { return new FlacDecoder( NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData); diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index b8b9598989..2e9638c447 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -79,8 +79,8 @@ public class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected int supportsFormatInternal(DrmSessionManager drmSessionManager, - Format format) { + protected int supportsFormatInternal( + @Nullable DrmSessionManager drmSessionManager, Format format) { boolean drmIsSupported = format.drmInitData == null || OpusLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType) @@ -99,7 +99,7 @@ public class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) + protected OpusDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) throws OpusDecoderException { int initialInputBufferSize = format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java index dbce33b923..d93036113c 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java @@ -44,7 +44,7 @@ import java.util.List; private static final int DECODE_ERROR = -1; private static final int DRM_ERROR = -2; - private final ExoMediaCrypto exoMediaCrypto; + @Nullable private final ExoMediaCrypto exoMediaCrypto; private final int channelCount; private final int headerSkipSamples; @@ -66,8 +66,13 @@ import java.util.List; * content. Maybe null and can be ignored if decoder does not handle encrypted content. * @throws OpusDecoderException Thrown if an exception occurs when initializing the decoder. */ - public OpusDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, - List initializationData, ExoMediaCrypto exoMediaCrypto) throws OpusDecoderException { + public OpusDecoder( + int numInputBuffers, + int numOutputBuffers, + int initialInputBufferSize, + List initializationData, + @Nullable ExoMediaCrypto exoMediaCrypto) + throws OpusDecoderException { super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]); if (!OpusLibrary.isAvailable()) { throw new OpusDecoderException("Failed to load decoder native libraries."); @@ -232,10 +237,22 @@ import java.util.List; int gain, byte[] streamMap); private native int opusDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize, SimpleOutputBuffer outputBuffer); - private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer, - int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate, - ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv, - int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData); + + private native int opusSecureDecode( + long decoder, + long timeUs, + ByteBuffer inputBuffer, + int inputSize, + SimpleOutputBuffer outputBuffer, + int sampleRate, + @Nullable ExoMediaCrypto mediaCrypto, + int inputMode, + byte[] key, + byte[] iv, + int numSubSamples, + int[] numBytesOfClearData, + int[] numBytesOfEncryptedData); + private native void opusClose(long decoder); private native void opusReset(long decoder); private native int opusGetErrorCode(long decoder); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index b17fa75181..e4691db7c0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -246,7 +246,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @return The extent to which the renderer supports the format itself. */ protected abstract int supportsFormatInternal( - DrmSessionManager drmSessionManager, Format format); + @Nullable DrmSessionManager drmSessionManager, Format format); /** * Returns whether the sink supports the audio format. @@ -341,9 +341,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @return The decoder. * @throws AudioDecoderException If an error occurred creating a suitable decoder. */ - protected abstract SimpleDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) - throws AudioDecoderException; + protected abstract SimpleDecoder< + DecoderInputBuffer, ? extends SimpleOutputBuffer, ? extends AudioDecoderException> + createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) + throws AudioDecoderException; /** * Returns the format of audio buffers output by the decoder. Will not be called until the first diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java index 950061e9bc..6769f5049b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -54,20 +55,22 @@ public class SimpleDecoderAudioRendererTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - audioRenderer = new SimpleDecoderAudioRenderer(null, null, null, false, mockAudioSink) { - @Override - protected int supportsFormatInternal(DrmSessionManager drmSessionManager, - Format format) { - return FORMAT_HANDLED; - } + audioRenderer = + new SimpleDecoderAudioRenderer(null, null, null, false, mockAudioSink) { + @Override + protected int supportsFormatInternal( + @Nullable DrmSessionManager drmSessionManager, Format format) { + return FORMAT_HANDLED; + } - @Override - protected SimpleDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) - throws AudioDecoderException { - return new FakeDecoder(); - } - }; + @Override + protected SimpleDecoder< + DecoderInputBuffer, ? extends SimpleOutputBuffer, ? extends AudioDecoderException> + createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) + throws AudioDecoderException { + return new FakeDecoder(); + } + }; } @Config(sdk = 19) From fd803a39a3470bb9b806d7cf76594e1192ee9d28 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 6 Aug 2019 16:18:33 +0100 Subject: [PATCH 1543/1556] Further MediaPeriod.selectTracks documentation tweak PiperOrigin-RevId: 261917229 --- .../google/android/exoplayer2/source/MediaPeriod.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index 847c87b077..3f306c0c8a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -113,15 +113,17 @@ public interface MediaPeriod extends SequenceableLoader { * corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set * if a new sample stream is created. * - *

          Note that previously received {@link TrackSelection TrackSelections} are no longer valid and - * references need to be replaced even if the corresponding {@link SampleStream} is kept. + *

          Note that previously passed {@link TrackSelection TrackSelections} are no longer valid, and + * any references to them must be updated to point to the new selections. * *

          This method is only called after the period has been prepared. * * @param selections The renderer track selections. * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained - * for each selection. A {@code true} value indicates that the selection is unchanged, and - * that the caller does not require that the sample stream be recreated. + * for each track selection. A {@code true} value indicates that the selection is equivalent + * to the one that was previously passed, and that the caller does not require that the sample + * stream be recreated. If a retained sample stream holds any references to the track + * selection then they must be updated to point to the new selection. * @param streams The existing sample streams, which will be updated to reflect the provided * selections. * @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that From 6617862f0b3a4a829892a51f01fa914b86104122 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 6 Aug 2019 17:28:15 +0100 Subject: [PATCH 1544/1556] Add allowAudioMixedChannelCountAdaptiveness parameter to DefaultTrackSelector. We already allow mixed mime type and mixed sample rate adaptation on request, so for completeness, we can also allow mixed channel count adaptation. Issue:#6257 PiperOrigin-RevId: 261930046 --- RELEASENOTES.md | 4 ++ .../trackselection/DefaultTrackSelector.java | 55 ++++++++++++++++--- .../DefaultTrackSelectorTest.java | 1 + 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 06cfde8d6c..7c934c478c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,6 +27,10 @@ over other selection parameters. * Remove `AnalyticsCollector.Factory`. Instances can be created directly and the `Player` set later using `AnalyticsCollector.setPlayer`. +* Add `allowAudioMixedChannelCountAdaptiveness` parameter to + `DefaultTrackSelector` to allow adaptive selections of audio tracks with + different channel counts + ([#6257](https://github.com/google/ExoPlayer/issues/6257)). ### 2.10.4 ### 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 cc1742bb31..77a7acc9e4 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 @@ -177,6 +177,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private boolean exceedAudioConstraintsIfNecessary; private boolean allowAudioMixedMimeTypeAdaptiveness; private boolean allowAudioMixedSampleRateAdaptiveness; + private boolean allowAudioMixedChannelCountAdaptiveness; // General private boolean forceLowestBitrate; private boolean forceHighestSupportedBitrate; @@ -227,6 +228,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { exceedAudioConstraintsIfNecessary = initialValues.exceedAudioConstraintsIfNecessary; allowAudioMixedMimeTypeAdaptiveness = initialValues.allowAudioMixedMimeTypeAdaptiveness; allowAudioMixedSampleRateAdaptiveness = initialValues.allowAudioMixedSampleRateAdaptiveness; + allowAudioMixedChannelCountAdaptiveness = + initialValues.allowAudioMixedChannelCountAdaptiveness; // General forceLowestBitrate = initialValues.forceLowestBitrate; forceHighestSupportedBitrate = initialValues.forceHighestSupportedBitrate; @@ -424,6 +427,17 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } + /** + * See {@link Parameters#allowAudioMixedChannelCountAdaptiveness}. + * + * @return This builder. + */ + public ParametersBuilder setAllowAudioMixedChannelCountAdaptiveness( + boolean allowAudioMixedChannelCountAdaptiveness) { + this.allowAudioMixedChannelCountAdaptiveness = allowAudioMixedChannelCountAdaptiveness; + return this; + } + // Text @Override @@ -640,6 +654,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { exceedAudioConstraintsIfNecessary, allowAudioMixedMimeTypeAdaptiveness, allowAudioMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness, // Text preferredTextLanguage, selectUndeterminedTextLanguage, @@ -775,6 +790,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { * different sample rates may not be completely seamless. The default value is {@code false}. */ public final boolean allowAudioMixedSampleRateAdaptiveness; + /** + * Whether to allow adaptive audio selections containing mixed channel counts. Adaptations + * between different channel counts may not be completely seamless. The default value is {@code + * false}. + */ + public final boolean allowAudioMixedChannelCountAdaptiveness; // General /** @@ -835,6 +856,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /* exceedAudioConstraintsIfNecessary= */ true, /* allowAudioMixedMimeTypeAdaptiveness= */ false, /* allowAudioMixedSampleRateAdaptiveness= */ false, + /* allowAudioMixedChannelCountAdaptiveness= */ false, // Text TrackSelectionParameters.DEFAULT.preferredTextLanguage, TrackSelectionParameters.DEFAULT.selectUndeterminedTextLanguage, @@ -867,6 +889,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean exceedAudioConstraintsIfNecessary, boolean allowAudioMixedMimeTypeAdaptiveness, boolean allowAudioMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness, // Text @Nullable String preferredTextLanguage, boolean selectUndeterminedTextLanguage, @@ -901,6 +924,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { this.exceedAudioConstraintsIfNecessary = exceedAudioConstraintsIfNecessary; this.allowAudioMixedMimeTypeAdaptiveness = allowAudioMixedMimeTypeAdaptiveness; this.allowAudioMixedSampleRateAdaptiveness = allowAudioMixedSampleRateAdaptiveness; + this.allowAudioMixedChannelCountAdaptiveness = allowAudioMixedChannelCountAdaptiveness; // General this.forceLowestBitrate = forceLowestBitrate; this.forceHighestSupportedBitrate = forceHighestSupportedBitrate; @@ -934,6 +958,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { this.exceedAudioConstraintsIfNecessary = Util.readBoolean(in); this.allowAudioMixedMimeTypeAdaptiveness = Util.readBoolean(in); this.allowAudioMixedSampleRateAdaptiveness = Util.readBoolean(in); + this.allowAudioMixedChannelCountAdaptiveness = Util.readBoolean(in); // General this.forceLowestBitrate = Util.readBoolean(in); this.forceHighestSupportedBitrate = Util.readBoolean(in); @@ -1015,6 +1040,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { && exceedAudioConstraintsIfNecessary == other.exceedAudioConstraintsIfNecessary && allowAudioMixedMimeTypeAdaptiveness == other.allowAudioMixedMimeTypeAdaptiveness && allowAudioMixedSampleRateAdaptiveness == other.allowAudioMixedSampleRateAdaptiveness + && allowAudioMixedChannelCountAdaptiveness + == other.allowAudioMixedChannelCountAdaptiveness // General && forceLowestBitrate == other.forceLowestBitrate && forceHighestSupportedBitrate == other.forceHighestSupportedBitrate @@ -1045,6 +1072,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { result = 31 * result + (exceedAudioConstraintsIfNecessary ? 1 : 0); result = 31 * result + (allowAudioMixedMimeTypeAdaptiveness ? 1 : 0); result = 31 * result + (allowAudioMixedSampleRateAdaptiveness ? 1 : 0); + result = 31 * result + (allowAudioMixedChannelCountAdaptiveness ? 1 : 0); // General result = 31 * result + (forceLowestBitrate ? 1 : 0); result = 31 * result + (forceHighestSupportedBitrate ? 1 : 0); @@ -1081,6 +1109,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { Util.writeBoolean(dest, exceedAudioConstraintsIfNecessary); Util.writeBoolean(dest, allowAudioMixedMimeTypeAdaptiveness); Util.writeBoolean(dest, allowAudioMixedSampleRateAdaptiveness); + Util.writeBoolean(dest, allowAudioMixedChannelCountAdaptiveness); // General Util.writeBoolean(dest, forceLowestBitrate); Util.writeBoolean(dest, forceHighestSupportedBitrate); @@ -1989,7 +2018,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { formatSupports[selectedGroupIndex], params.maxAudioBitrate, params.allowAudioMixedMimeTypeAdaptiveness, - params.allowAudioMixedSampleRateAdaptiveness); + params.allowAudioMixedSampleRateAdaptiveness, + params.allowAudioMixedChannelCountAdaptiveness); if (adaptiveTracks.length > 0) { definition = new TrackSelection.Definition(selectedGroup, adaptiveTracks); } @@ -2007,7 +2037,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { int[] formatSupport, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, - boolean allowMixedSampleRateAdaptiveness) { + boolean allowMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness) { int selectedConfigurationTrackCount = 0; AudioConfigurationTuple selectedConfiguration = null; HashSet seenConfigurationTuples = new HashSet<>(); @@ -2024,7 +2055,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { configuration, maxAudioBitrate, allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness); + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness); if (configurationCount > selectedConfigurationTrackCount) { selectedConfiguration = configuration; selectedConfigurationTrackCount = configurationCount; @@ -2044,7 +2076,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { selectedConfiguration, maxAudioBitrate, allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness)) { + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness)) { adaptiveIndices[index++] = i; } } @@ -2059,7 +2092,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { AudioConfigurationTuple configuration, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, - boolean allowMixedSampleRateAdaptiveness) { + boolean allowMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness) { int count = 0; for (int i = 0; i < group.length; i++) { if (isSupportedAdaptiveAudioTrack( @@ -2068,7 +2102,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { configuration, maxAudioBitrate, allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness)) { + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness)) { count++; } } @@ -2081,11 +2116,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { AudioConfigurationTuple configuration, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, - boolean allowMixedSampleRateAdaptiveness) { + boolean allowMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness) { return isSupported(formatSupport, false) && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxAudioBitrate) - && (format.channelCount != Format.NO_VALUE - && format.channelCount == configuration.channelCount) + && (allowAudioMixedChannelCountAdaptiveness + || (format.channelCount != Format.NO_VALUE + && format.channelCount == configuration.channelCount)) && (allowMixedMimeTypeAdaptiveness || (format.sampleMimeType != null && TextUtils.equals(format.sampleMimeType, configuration.mimeType))) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index 4622dc1734..0374f88bae 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -148,6 +148,7 @@ public final class DefaultTrackSelectorTest { /* exceedAudioConstraintsIfNecessary= */ false, /* allowAudioMixedMimeTypeAdaptiveness= */ true, /* allowAudioMixedSampleRateAdaptiveness= */ false, + /* allowAudioMixedChannelCountAdaptiveness= */ true, // Text /* preferredTextLanguage= */ "de", /* selectUndeterminedTextLanguage= */ true, From 113e25dc740490104c69afb454315ca210b0b23d Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 7 Aug 2019 11:36:41 +0100 Subject: [PATCH 1545/1556] Clean up documentation of DefaultTrackSelector.ParametersBuilder. We don't usually refer to other classes when documenting method parameters but rather duplicate the actual definition. PiperOrigin-RevId: 262102714 --- .../trackselection/DefaultTrackSelector.java | 131 +++++++++++++----- .../TrackSelectionParameters.java | 20 ++- 2 files changed, 109 insertions(+), 42 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 77a7acc9e4..8e1284f7ef 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 @@ -261,8 +261,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#maxVideoWidth} and {@link Parameters#maxVideoHeight}. + * Sets the maximum allowed video width and height. * + * @param maxVideoWidth Maximum allowed video width in pixels. + * @param maxVideoHeight Maximum allowed video height in pixels. * @return This builder. */ public ParametersBuilder setMaxVideoSize(int maxVideoWidth, int maxVideoHeight) { @@ -272,8 +274,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#maxVideoFrameRate}. + * Sets the maximum allowed video frame rate. * + * @param maxVideoFrameRate Maximum allowed video frame rate in hertz. * @return This builder. */ public ParametersBuilder setMaxVideoFrameRate(int maxVideoFrameRate) { @@ -282,8 +285,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#maxVideoBitrate}. + * Sets the maximum allowed video bitrate. * + * @param maxVideoBitrate Maximum allowed video bitrate in bits per second. * @return This builder. */ public ParametersBuilder setMaxVideoBitrate(int maxVideoBitrate) { @@ -292,8 +296,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#exceedVideoConstraintsIfNecessary}. + * Sets whether to exceed the {@link #setMaxVideoSize(int, int)} and {@link + * #setMaxAudioBitrate(int)} constraints when no selection can be made otherwise. * + * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no + * selection can be made otherwise. * @return This builder. */ public ParametersBuilder setExceedVideoConstraintsIfNecessary( @@ -303,8 +310,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#allowVideoMixedMimeTypeAdaptiveness}. + * Sets whether to allow adaptive video selections containing mixed MIME types. * + *

          Adaptations between different MIME types may not be completely seamless, in which case + * {@link #setAllowVideoNonSeamlessAdaptiveness(boolean)} also needs to be {@code true} for + * mixed MIME type selections to be made. + * + * @param allowVideoMixedMimeTypeAdaptiveness Whether to allow adaptive video selections + * containing mixed MIME types. * @return This builder. */ public ParametersBuilder setAllowVideoMixedMimeTypeAdaptiveness( @@ -314,8 +327,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#allowVideoNonSeamlessAdaptiveness}. + * Sets whether to allow adaptive video selections where adaptation may not be completely + * seamless. * + * @param allowVideoNonSeamlessAdaptiveness Whether to allow adaptive video selections where + * adaptation may not be completely seamless. * @return This builder. */ public ParametersBuilder setAllowVideoNonSeamlessAdaptiveness( @@ -329,7 +345,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * obtained from {@link Util#getPhysicalDisplaySize(Context)}. * * @param context Any context. - * @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}. + * @param viewportOrientationMayChange Whether the viewport orientation may change during + * playback. * @return This builder. */ public ParametersBuilder setViewportSizeToPhysicalDisplaySize( @@ -350,12 +367,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and {@link - * Parameters#viewportOrientationMayChange}. + * Sets the viewport size to constrain adaptive video selections so that only tracks suitable + * for the viewport are selected. * - * @param viewportWidth See {@link Parameters#viewportWidth}. - * @param viewportHeight See {@link Parameters#viewportHeight}. - * @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}. + * @param viewportWidth Viewport width in pixels. + * @param viewportHeight Viewport height in pixels. + * @param viewportOrientationMayChange Whether the viewport orientation may change during + * playback. * @return This builder. */ public ParametersBuilder setViewportSize( @@ -375,8 +393,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#maxAudioChannelCount}. + * Sets the maximum allowed audio channel count. * + * @param maxAudioChannelCount Maximum allowed audio channel count. * @return This builder. */ public ParametersBuilder setMaxAudioChannelCount(int maxAudioChannelCount) { @@ -385,8 +404,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#maxAudioBitrate}. + * Sets the maximum allowed audio bitrate. * + * @param maxAudioBitrate Maximum allowed audio bitrate in bits per second. * @return This builder. */ public ParametersBuilder setMaxAudioBitrate(int maxAudioBitrate) { @@ -395,8 +415,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#exceedAudioConstraintsIfNecessary}. + * Sets whether to exceed the {@link #setMaxAudioChannelCount(int)} and {@link + * #setMaxAudioBitrate(int)} constraints when no selection can be made otherwise. * + * @param exceedAudioConstraintsIfNecessary Whether to exceed audio constraints when no + * selection can be made otherwise. * @return This builder. */ public ParametersBuilder setExceedAudioConstraintsIfNecessary( @@ -406,8 +429,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#allowAudioMixedMimeTypeAdaptiveness}. + * Sets whether to allow adaptive audio selections containing mixed MIME types. * + *

          Adaptations between different MIME types may not be completely seamless. + * + * @param allowAudioMixedMimeTypeAdaptiveness Whether to allow adaptive audio selections + * containing mixed MIME types. * @return This builder. */ public ParametersBuilder setAllowAudioMixedMimeTypeAdaptiveness( @@ -417,8 +444,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#allowAudioMixedSampleRateAdaptiveness}. + * Sets whether to allow adaptive audio selections containing mixed sample rates. * + *

          Adaptations between different sample rates may not be completely seamless. + * + * @param allowAudioMixedSampleRateAdaptiveness Whether to allow adaptive audio selections + * containing mixed sample rates. * @return This builder. */ public ParametersBuilder setAllowAudioMixedSampleRateAdaptiveness( @@ -428,8 +459,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#allowAudioMixedChannelCountAdaptiveness}. + * Sets whether to allow adaptive audio selections containing mixed channel counts. * + *

          Adaptations between different channel counts may not be completely seamless. + * + * @param allowAudioMixedChannelCountAdaptiveness Whether to allow adaptive audio selections + * containing mixed channel counts. * @return This builder. */ public ParametersBuilder setAllowAudioMixedChannelCountAdaptiveness( @@ -462,8 +497,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { // General /** - * See {@link Parameters#forceLowestBitrate}. + * Sets whether to force selection of the single lowest bitrate audio and video tracks that + * comply with all other constraints. * + * @param forceLowestBitrate Whether to force selection of the single lowest bitrate audio and + * video tracks. * @return This builder. */ public ParametersBuilder setForceLowestBitrate(boolean forceLowestBitrate) { @@ -472,8 +510,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#forceHighestSupportedBitrate}. + * Sets whether to force selection of the highest bitrate audio and video tracks that comply + * with all other constraints. * + * @param forceHighestSupportedBitrate Whether to force selection of the highest bitrate audio + * and video tracks. * @return This builder. */ public ParametersBuilder setForceHighestSupportedBitrate(boolean forceHighestSupportedBitrate) { @@ -499,8 +540,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#exceedRendererCapabilitiesIfNecessary}. + * Sets 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. + * + * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no + * selection can be made otherwise. * @return This builder. */ public ParametersBuilder setExceedRendererCapabilitiesIfNecessary( @@ -510,7 +558,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#tunnelingAudioSessionId}. + * Sets the audio session id to use when tunneling. * *

          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 @@ -520,6 +568,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * * @param tunnelingAudioSessionId The audio session id to use when tunneling, or {@link * C#AUDIO_SESSION_ID_UNSET} to disable tunneling. + * @return This builder. */ public ParametersBuilder setTunnelingAudioSessionId(int tunnelingAudioSessionId) { this.tunnelingAudioSessionId = tunnelingAudioSessionId; @@ -534,6 +583,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * * @param rendererIndex The renderer index. * @param disabled Whether the renderer is disabled. + * @return This builder. */ public final ParametersBuilder setRendererDisabled(int rendererIndex, boolean disabled) { if (rendererDisabledFlags.get(rendererIndex) == disabled) { @@ -570,6 +620,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param rendererIndex The renderer index. * @param groups The {@link TrackGroupArray} for which the override should be applied. * @param override The override. + * @return This builder. */ public final ParametersBuilder setSelectionOverride( int rendererIndex, TrackGroupArray groups, SelectionOverride override) { @@ -591,6 +642,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * * @param rendererIndex The renderer index. * @param groups The {@link TrackGroupArray} for which the override should be cleared. + * @return This builder. */ public final ParametersBuilder clearSelectionOverride( int rendererIndex, TrackGroupArray groups) { @@ -610,6 +662,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * Clears all track selection overrides for the specified renderer. * * @param rendererIndex The renderer index. + * @return This builder. */ public final ParametersBuilder clearSelectionOverrides(int rendererIndex) { Map overrides = selectionOverrides.get(rendererIndex); @@ -621,7 +674,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } - /** Clears all track selection overrides for all renderers. */ + /** + * Clears all track selection overrides for all renderers. + * + * @return This builder. + */ public final ParametersBuilder clearSelectionOverrides() { if (selectionOverrides.size() == 0) { // Nothing to clear. @@ -703,8 +760,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Video /** - * Maximum allowed video width. The default value is {@link Integer#MAX_VALUE} (i.e. no - * constraint). + * Maximum allowed video width in pixels. The default value is {@link Integer#MAX_VALUE} (i.e. + * no constraint). * *

          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 @@ -712,8 +769,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final int maxVideoWidth; /** - * Maximum allowed video height. The default value is {@link Integer#MAX_VALUE} (i.e. no - * constraint). + * Maximum allowed video height in pixels. The default value is {@link Integer#MAX_VALUE} (i.e. + * no constraint). * *

          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 @@ -721,12 +778,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final int maxVideoHeight; /** - * Maximum allowed video frame rate. The default value is {@link Integer#MAX_VALUE} (i.e. no - * constraint). + * Maximum allowed video frame rate in hertz. The default value is {@link Integer#MAX_VALUE} + * (i.e. no constraint). */ public final int maxVideoFrameRate; /** - * Maximum video bitrate. The default value is {@link Integer#MAX_VALUE} (i.e. no constraint). + * Maximum allowed video bitrate in bits per second. The default value is {@link + * Integer#MAX_VALUE} (i.e. no constraint). */ public final int maxVideoBitrate; /** @@ -736,9 +794,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final boolean exceedVideoConstraintsIfNecessary; /** - * Whether to allow adaptive video selections containing mixed mime types. Adaptations between - * different mime types may not be completely seamless, in which case {@link - * #allowVideoNonSeamlessAdaptiveness} also needs to be {@code true} for mixed mime type + * Whether to allow adaptive video selections containing mixed MIME types. Adaptations between + * different MIME types may not be completely seamless, in which case {@link + * #allowVideoNonSeamlessAdaptiveness} also needs to be {@code true} for mixed MIME type * selections to be made. The default value is {@code false}. */ public final boolean allowVideoMixedMimeTypeAdaptiveness; @@ -772,7 +830,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final int maxAudioChannelCount; /** - * Maximum audio bitrate. The default value is {@link Integer#MAX_VALUE} (i.e. no constraint). + * Maximum allowed audio bitrate in bits per second. The default value is {@link + * Integer#MAX_VALUE} (i.e. no constraint). */ public final int maxAudioBitrate; /** @@ -781,8 +840,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final boolean exceedAudioConstraintsIfNecessary; /** - * Whether to allow adaptive audio selections containing mixed mime types. Adaptations between - * different mime types may not be completely seamless. The default value is {@code false}. + * Whether to allow adaptive audio selections containing mixed MIME types. Adaptations between + * different MIME types may not be completely seamless. The default value is {@code false}. */ public final boolean allowAudioMixedMimeTypeAdaptiveness; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java index 81af551b68..c406f262d1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java @@ -53,9 +53,10 @@ public class TrackSelectionParameters implements Parcelable { } /** - * See {@link TrackSelectionParameters#preferredAudioLanguage}. + * Sets the preferred language for audio and forced text tracks. * - * @param preferredAudioLanguage Preferred audio language as an IETF BCP 47 conformant tag. + * @param preferredAudioLanguage Preferred audio language as an IETF BCP 47 conformant tag, or + * {@code null} to select the default track, or the first track if there's no default. * @return This builder. */ public Builder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) { @@ -64,9 +65,10 @@ public class TrackSelectionParameters implements Parcelable { } /** - * See {@link TrackSelectionParameters#preferredTextLanguage}. + * Sets the preferred language for text tracks. * - * @param preferredTextLanguage Preferred text language as an IETF BCP 47 conformant tag. + * @param preferredTextLanguage Preferred text language as an IETF BCP 47 conformant tag, or + * {@code null} to select the default track if there is one, or no track otherwise. * @return This builder. */ public Builder setPreferredTextLanguage(@Nullable String preferredTextLanguage) { @@ -75,8 +77,12 @@ public class TrackSelectionParameters implements Parcelable { } /** - * See {@link TrackSelectionParameters#selectUndeterminedTextLanguage}. + * Sets whether a text track with undetermined language should be selected if no track with + * {@link #setPreferredTextLanguage(String)} is available, or if the preferred language is + * unset. * + * @param selectUndeterminedTextLanguage Whether a text track with undetermined language should + * be selected if no preferred language track is available. * @return This builder. */ public Builder setSelectUndeterminedTextLanguage(boolean selectUndeterminedTextLanguage) { @@ -85,8 +91,10 @@ public class TrackSelectionParameters implements Parcelable { } /** - * See {@link TrackSelectionParameters#disabledTextTrackSelectionFlags}. + * Sets a bitmask of selection flags that are disabled for text track selections. * + * @param disabledTextTrackSelectionFlags A bitmask of {@link C.SelectionFlags} that are + * disabled for text track selections. * @return This builder. */ public Builder setDisabledTextTrackSelectionFlags( From 79e962c55a42844cde02710fec1e9644c9221cc5 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 7 Aug 2019 14:18:22 +0100 Subject: [PATCH 1546/1556] Expose a method on EventMessageDecoder that returns EventMessage directly PiperOrigin-RevId: 262121134 --- .../exoplayer2/metadata/emsg/EventMessageDecoder.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index a49bf956b3..340b662e97 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -37,7 +37,10 @@ public final class EventMessageDecoder implements MetadataDecoder { ByteBuffer buffer = inputBuffer.data; byte[] data = buffer.array(); int size = buffer.limit(); - ParsableByteArray emsgData = new ParsableByteArray(data, size); + return new Metadata(decode(new ParsableByteArray(data, size))); + } + + public EventMessage decode(ParsableByteArray emsgData) { String schemeIdUri = Assertions.checkNotNull(emsgData.readNullTerminatedString()); String value = Assertions.checkNotNull(emsgData.readNullTerminatedString()); long timescale = emsgData.readUnsignedInt(); @@ -50,8 +53,9 @@ public final class EventMessageDecoder implements MetadataDecoder { long durationMs = Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); long id = emsgData.readUnsignedInt(); - byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size); - return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData)); + byte[] messageData = + Arrays.copyOfRange(emsgData.data, emsgData.getPosition(), emsgData.limit()); + return new EventMessage(schemeIdUri, value, durationMs, id, messageData); } } From 074b6f8ebd14371ccfa699fadacb8b15dcdc1b57 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 7 Aug 2019 14:36:57 +0100 Subject: [PATCH 1547/1556] Fix DASH module API nullability issues and add package-level non-null-by-default PiperOrigin-RevId: 262123595 --- library/dash/build.gradle | 1 + .../source/dash/DashMediaPeriod.java | 9 +- .../source/dash/DashMediaSource.java | 30 +++--- .../exoplayer2/source/dash/DashUtil.java | 16 ++-- .../source/dash/DefaultDashChunkSource.java | 6 +- .../source/dash/manifest/DashManifest.java | 16 ++-- .../dash/manifest/DashManifestParser.java | 94 +++++++++++-------- .../source/dash/manifest/Descriptor.java | 12 +-- .../dash/manifest/ProgramInformation.java | 16 ++-- .../source/dash/manifest/RangedUri.java | 2 +- .../source/dash/manifest/Representation.java | 45 +++++---- .../source/dash/manifest/SegmentBase.java | 50 ++++++---- .../source/dash/manifest/package-info.java | 19 ++++ .../source/dash/offline/package-info.java | 19 ++++ .../exoplayer2/source/dash/package-info.java | 19 ++++ 15 files changed, 228 insertions(+), 126 deletions(-) create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/package-info.java create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/package-info.java create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/package-info.java diff --git a/library/dash/build.gradle b/library/dash/build.gradle index 9f5775d478..c34ed8c907 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -41,6 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') } 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 5daa1a8fd5..21fd43da21 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 @@ -60,6 +60,7 @@ import java.util.IdentityHashMap; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** A DASH {@link MediaPeriod}. */ /* package */ final class DashMediaPeriod @@ -245,8 +246,12 @@ import java.util.regex.Pattern; } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { int[] streamIndexToTrackGroupIndex = getStreamIndexToTrackGroupIndex(selections); releaseDisabledStreams(selections, mayRetainStreamFlags, streams); releaseOrphanEmbeddedStreams(selections, streams, streamIndexToTrackGroupIndex); 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 576491b464..890a272c5e 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 @@ -130,7 +130,7 @@ public final class DashMediaSource extends BaseMediaSource { * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setTag(Object tag) { + public Factory setTag(@Nullable Object tag) { Assertions.checkState(!isCreateCalled); this.tag = tag; return this; @@ -430,8 +430,8 @@ public final class DashMediaSource extends BaseMediaSource { public DashMediaSource( DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifest, chunkSourceFactory, @@ -455,8 +455,8 @@ public final class DashMediaSource extends BaseMediaSource { DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifest, /* manifestUri= */ null, @@ -492,8 +492,8 @@ public final class DashMediaSource extends BaseMediaSource { Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifestUri, manifestDataSourceFactory, @@ -529,8 +529,8 @@ public final class DashMediaSource extends BaseMediaSource { DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifestUri, manifestDataSourceFactory, @@ -569,8 +569,8 @@ public final class DashMediaSource extends BaseMediaSource { DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( /* manifest= */ null, manifestUri, @@ -591,10 +591,10 @@ public final class DashMediaSource extends BaseMediaSource { } private DashMediaSource( - DashManifest manifest, - Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, - ParsingLoadable.Parser manifestParser, + @Nullable DashManifest manifest, + @Nullable Uri manifestUri, + @Nullable DataSource.Factory manifestDataSourceFactory, + @Nullable ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, DrmSessionManager drmSessionManager, diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java index 6a6e08ce1d..c9433b9e41 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java @@ -66,7 +66,8 @@ public final class DashUtil { * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static @Nullable DrmInitData loadDrmInitData(DataSource dataSource, Period period) + @Nullable + public static DrmInitData loadDrmInitData(DataSource dataSource, Period period) throws IOException, InterruptedException { int primaryTrackType = C.TRACK_TYPE_VIDEO; Representation representation = getFirstRepresentation(period, primaryTrackType); @@ -95,7 +96,8 @@ public final class DashUtil { * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static @Nullable Format loadSampleFormat( + @Nullable + public static Format loadSampleFormat( DataSource dataSource, int trackType, Representation representation) throws IOException, InterruptedException { ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, trackType, @@ -116,7 +118,8 @@ public final class DashUtil { * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static @Nullable ChunkIndex loadChunkIndex( + @Nullable + public static ChunkIndex loadChunkIndex( DataSource dataSource, int trackType, Representation representation) throws IOException, InterruptedException { ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, trackType, @@ -138,7 +141,8 @@ public final class DashUtil { * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - private static @Nullable ChunkExtractorWrapper loadInitializationData( + @Nullable + private static ChunkExtractorWrapper loadInitializationData( DataSource dataSource, int trackType, Representation representation, boolean loadIndex) throws IOException, InterruptedException { RangedUri initializationUri = representation.getInitializationUri(); @@ -187,7 +191,8 @@ public final class DashUtil { return new ChunkExtractorWrapper(extractor, trackType, format); } - private static @Nullable Representation getFirstRepresentation(Period period, int type) { + @Nullable + private static Representation getFirstRepresentation(Period period, int type) { int index = period.getAdaptationSetIndex(type); if (index == C.INDEX_UNSET) { return null; @@ -197,5 +202,4 @@ public final class DashUtil { } private DashUtil() {} - } 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 bcf0a1766a..cd39c9538a 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 @@ -67,7 +67,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private final int maxSegmentsPerLoad; public Factory(DataSource.Factory dataSourceFactory) { - this(dataSourceFactory, 1); + this(dataSourceFactory, /* maxSegmentsPerLoad= */ 1); } public Factory(DataSource.Factory dataSourceFactory, int maxSegmentsPerLoad) { @@ -633,7 +633,7 @@ public class DefaultDashChunkSource implements DashChunkSource { Representation representation, boolean enableEventMessageTrack, List closedCaptionFormats, - TrackOutput playerEmsgTrackOutput) { + @Nullable TrackOutput playerEmsgTrackOutput) { this( periodDurationUs, representation, @@ -787,7 +787,7 @@ public class DefaultDashChunkSource implements DashChunkSource { Representation representation, boolean enableEventMessageTrack, List closedCaptionFormats, - TrackOutput playerEmsgTrackOutput) { + @Nullable TrackOutput playerEmsgTrackOutput) { String containerMimeType = representation.format.containerMimeType; if (mimeTypeIsRawText(containerMimeType)) { return null; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index 0c3f641cbe..2d8909f8b4 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -80,12 +80,10 @@ public class DashManifest implements FilterableManifest { * The {@link UtcTimingElement}, or null if not present. Defined in DVB A168:7/2016, Section * 4.7.2. */ - public final UtcTimingElement utcTiming; + @Nullable public final UtcTimingElement utcTiming; - /** - * The location of this manifest. - */ - public final Uri location; + /** The location of this manifest, or null if not present. */ + @Nullable public final Uri location; /** The {@link ProgramInformation}, or null if not present. */ @Nullable public final ProgramInformation programInformation; @@ -106,8 +104,8 @@ public class DashManifest implements FilterableManifest { long timeShiftBufferDepthMs, long suggestedPresentationDelayMs, long publishTimeMs, - UtcTimingElement utcTiming, - Uri location, + @Nullable UtcTimingElement utcTiming, + @Nullable Uri location, List periods) { this( availabilityStartTimeMs, @@ -134,8 +132,8 @@ public class DashManifest implements FilterableManifest { long suggestedPresentationDelayMs, long publishTimeMs, @Nullable ProgramInformation programInformation, - UtcTimingElement utcTiming, - Uri location, + @Nullable UtcTimingElement utcTiming, + @Nullable Uri location, List periods) { this.availabilityStartTimeMs = availabilityStartTimeMs; this.durationMs = durationMs; 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 1419f8198c..8affcb27ce 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.dash.manifest; import android.net.Uri; +import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Base64; import android.util.Pair; @@ -47,6 +48,7 @@ import java.util.List; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.compatqual.NullableType; import org.xml.sax.helpers.DefaultHandler; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -189,9 +191,9 @@ public class DashManifestParser extends DefaultHandler long timeShiftBufferDepthMs, long suggestedPresentationDelayMs, long publishTimeMs, - ProgramInformation programInformation, - UtcTimingElement utcTiming, - Uri location, + @Nullable ProgramInformation programInformation, + @Nullable UtcTimingElement utcTiming, + @Nullable Uri location, List periods) { return new DashManifest( availabilityStartTime, @@ -259,8 +261,9 @@ public class DashManifestParser extends DefaultHandler // AdaptationSet parsing. - protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl, - SegmentBase segmentBase) throws XmlPullParserException, IOException { + protected AdaptationSet parseAdaptationSet( + XmlPullParser xpp, String baseUrl, @Nullable SegmentBase segmentBase) + throws XmlPullParserException, IOException { int id = parseInt(xpp, "id", AdaptationSet.ID_UNSET); int contentType = parseContentType(xpp); @@ -394,8 +397,8 @@ public class DashManifestParser extends DefaultHandler * @return The scheme type and/or {@link SchemeData} parsed from the ContentProtection element. * Either or both may be null, depending on the ContentProtection element being parsed. */ - protected Pair parseContentProtection(XmlPullParser xpp) - throws XmlPullParserException, IOException { + protected Pair<@NullableType String, @NullableType SchemeData> parseContentProtection( + XmlPullParser xpp) throws XmlPullParserException, IOException { String schemeType = null; String licenseServerUrl = null; byte[] data = null; @@ -477,19 +480,19 @@ public class DashManifestParser extends DefaultHandler protected RepresentationInfo parseRepresentation( XmlPullParser xpp, String baseUrl, - String label, - String adaptationSetMimeType, - String adaptationSetCodecs, + @Nullable String label, + @Nullable String adaptationSetMimeType, + @Nullable String adaptationSetCodecs, int adaptationSetWidth, int adaptationSetHeight, float adaptationSetFrameRate, int adaptationSetAudioChannels, int adaptationSetAudioSamplingRate, - String adaptationSetLanguage, + @Nullable String adaptationSetLanguage, List adaptationSetRoleDescriptors, List adaptationSetAccessibilityDescriptors, List adaptationSetSupplementalProperties, - SegmentBase segmentBase) + @Nullable SegmentBase segmentBase) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE); @@ -564,19 +567,19 @@ public class DashManifestParser extends DefaultHandler } protected Format buildFormat( - String id, - String label, - String containerMimeType, + @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, int width, int height, float frameRate, int audioChannels, int audioSamplingRate, int bitrate, - String language, + @Nullable String language, List roleDescriptors, List accessibilityDescriptors, - String codecs, + @Nullable String codecs, List supplementalProperties) { String sampleMimeType = getSampleMimeType(containerMimeType, codecs); @C.SelectionFlags int selectionFlags = parseSelectionFlagsFromRoleDescriptors(roleDescriptors); @@ -650,7 +653,7 @@ public class DashManifestParser extends DefaultHandler protected Representation buildRepresentation( RepresentationInfo representationInfo, - String extraDrmSchemeType, + @Nullable String extraDrmSchemeType, ArrayList extraDrmSchemeDatas, ArrayList extraInbandEventStreams) { Format format = representationInfo.format; @@ -675,7 +678,8 @@ public class DashManifestParser extends DefaultHandler // SegmentBase, SegmentList and SegmentTemplate parsing. - protected SingleSegmentBase parseSegmentBase(XmlPullParser xpp, SingleSegmentBase parent) + protected SingleSegmentBase parseSegmentBase( + XmlPullParser xpp, @Nullable SingleSegmentBase parent) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -711,7 +715,7 @@ public class DashManifestParser extends DefaultHandler indexLength); } - protected SegmentList parseSegmentList(XmlPullParser xpp, SegmentList parent) + protected SegmentList parseSegmentList(XmlPullParser xpp, @Nullable SegmentList parent) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -756,15 +760,15 @@ public class DashManifestParser extends DefaultHandler long presentationTimeOffset, long startNumber, long duration, - List timeline, - List segments) { + @Nullable List timeline, + @Nullable List segments) { return new SegmentList(initialization, timescale, presentationTimeOffset, startNumber, duration, timeline, segments); } protected SegmentTemplate parseSegmentTemplate( XmlPullParser xpp, - SegmentTemplate parent, + @Nullable SegmentTemplate parent, List adaptationSetSupplementalProperties) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -819,8 +823,8 @@ public class DashManifestParser extends DefaultHandler long endNumber, long duration, List timeline, - UrlTemplate initializationTemplate, - UrlTemplate mediaTemplate) { + @Nullable UrlTemplate initializationTemplate, + @Nullable UrlTemplate mediaTemplate) { return new SegmentTemplate( initialization, timescale, @@ -1008,8 +1012,9 @@ public class DashManifestParser extends DefaultHandler return new SegmentTimelineElement(elapsedTime, duration); } - protected UrlTemplate parseUrlTemplate(XmlPullParser xpp, String name, - UrlTemplate defaultValue) { + @Nullable + protected UrlTemplate parseUrlTemplate( + XmlPullParser xpp, String name, @Nullable UrlTemplate defaultValue) { String valueString = xpp.getAttributeValue(null, name); if (valueString != null) { return UrlTemplate.compile(valueString); @@ -1126,7 +1131,7 @@ public class DashManifestParser extends DefaultHandler } @C.RoleFlags - protected int parseDashRoleSchemeValue(String value) { + protected int parseDashRoleSchemeValue(@Nullable String value) { if (value == null) { return 0; } @@ -1159,7 +1164,7 @@ public class DashManifestParser extends DefaultHandler } @C.RoleFlags - protected int parseTvaAudioPurposeCsValue(String value) { + protected int parseTvaAudioPurposeCsValue(@Nullable String value) { if (value == null) { return 0; } @@ -1230,7 +1235,9 @@ public class DashManifestParser extends DefaultHandler * @param codecs The codecs attribute. * @return The derived sample mimeType, or null if it could not be derived. */ - private static String getSampleMimeType(String containerMimeType, String codecs) { + @Nullable + private static String getSampleMimeType( + @Nullable String containerMimeType, @Nullable String codecs) { if (MimeTypes.isAudio(containerMimeType)) { return MimeTypes.getAudioMediaMimeType(codecs); } else if (MimeTypes.isVideo(containerMimeType)) { @@ -1264,7 +1271,7 @@ public class DashManifestParser extends DefaultHandler * @param mimeType The mimeType. * @return Whether the mimeType is a text sample mimeType. */ - private static boolean mimeTypeIsRawText(String mimeType) { + private static boolean mimeTypeIsRawText(@Nullable String mimeType) { return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType) || MimeTypes.APPLICATION_MP4VTT.equals(mimeType) @@ -1273,16 +1280,18 @@ public class DashManifestParser extends DefaultHandler } /** - * Checks two languages for consistency, returning the consistent language, or throwing an - * {@link IllegalStateException} if the languages are inconsistent. - *

          - * Two languages are consistent if they are equal, or if one is null. + * Checks two languages for consistency, returning the consistent language, or throwing an {@link + * IllegalStateException} if the languages are inconsistent. + * + *

          Two languages are consistent if they are equal, or if one is null. * * @param firstLanguage The first language. * @param secondLanguage The second language. * @return The consistent language. */ - private static String checkLanguageConsistency(String firstLanguage, String secondLanguage) { + @Nullable + private static String checkLanguageConsistency( + @Nullable String firstLanguage, @Nullable String secondLanguage) { if (firstLanguage == null) { return secondLanguage; } else if (secondLanguage == null) { @@ -1485,14 +1494,19 @@ public class DashManifestParser extends DefaultHandler public final Format format; public final String baseUrl; public final SegmentBase segmentBase; - public final String drmSchemeType; + @Nullable public final String drmSchemeType; public final ArrayList drmSchemeDatas; public final ArrayList inbandEventStreams; public final long revisionId; - public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase, - String drmSchemeType, ArrayList drmSchemeDatas, - ArrayList inbandEventStreams, long revisionId) { + public RepresentationInfo( + Format format, + String baseUrl, + SegmentBase segmentBase, + @Nullable String drmSchemeType, + ArrayList drmSchemeDatas, + ArrayList inbandEventStreams, + long revisionId) { this.format = format; this.baseUrl = baseUrl; this.segmentBase = segmentBase; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java index 493a8da09c..d68690d363 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.source.dash.manifest; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Util; @@ -24,10 +23,8 @@ import com.google.android.exoplayer2.util.Util; */ public final class Descriptor { - /** - * The scheme URI. - */ - @NonNull public final String schemeIdUri; + /** The scheme URI. */ + public final String schemeIdUri; /** * The value, or null. */ @@ -42,7 +39,7 @@ public final class Descriptor { * @param value The value, or null. * @param id The identifier, or null. */ - public Descriptor(@NonNull String schemeIdUri, @Nullable String value, @Nullable String id) { + public Descriptor(String schemeIdUri, @Nullable String value, @Nullable String id) { this.schemeIdUri = schemeIdUri; this.value = value; this.id = id; @@ -63,10 +60,9 @@ public final class Descriptor { @Override public int hashCode() { - int result = (schemeIdUri != null ? schemeIdUri.hashCode() : 0); + int result = schemeIdUri.hashCode(); result = 31 * result + (value != null ? value.hashCode() : 0); result = 31 * result + (id != null ? id.hashCode() : 0); return result; } - } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/ProgramInformation.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/ProgramInformation.java index 62934d7433..ac264bd2b1 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/ProgramInformation.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/ProgramInformation.java @@ -21,22 +21,26 @@ import com.google.android.exoplayer2.util.Util; /** A parsed program information element. */ public class ProgramInformation { /** The title for the media presentation. */ - public final String title; + @Nullable public final String title; /** Information about the original source of the media presentation. */ - public final String source; + @Nullable public final String source; /** A copyright statement for the media presentation. */ - public final String copyright; + @Nullable public final String copyright; /** A URL that provides more information about the media presentation. */ - public final String moreInformationURL; + @Nullable public final String moreInformationURL; /** Declares the language code(s) for this ProgramInformation. */ - public final String lang; + @Nullable public final String lang; public ProgramInformation( - String title, String source, String copyright, String moreInformationURL, String lang) { + @Nullable String title, + @Nullable String source, + @Nullable String copyright, + @Nullable String moreInformationURL, + @Nullable String lang) { this.title = title; this.source = source; this.copyright = copyright; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java index 9ac1257ee2..bcd783f0cb 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java @@ -83,7 +83,7 @@ public final class RangedUri { *

          If {@code other} is null then the merge is considered unsuccessful, and null is returned. * * @param other The {@link RangedUri} to merge. - * @param baseUri The optional base Uri. + * @param baseUri The base Uri. * @return The merged {@link RangedUri} if the merge was successful. Null otherwise. */ @Nullable diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java index 0884bcc65c..80ad15cd8f 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.dash.manifest; import android.net.Uri; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.dash.DashSegmentIndex; @@ -53,9 +54,7 @@ public abstract class Representation { * The offset of the presentation timestamps in the media stream relative to media time. */ public final long presentationTimeOffsetUs; - /** - * The in-band event streams in the representation. Never null, but may be empty. - */ + /** The in-band event streams in the representation. May be empty. */ public final List inbandEventStreams; private final RangedUri initializationUri; @@ -71,7 +70,7 @@ public abstract class Representation { */ public static Representation newInstance( long revisionId, Format format, String baseUrl, SegmentBase segmentBase) { - return newInstance(revisionId, format, baseUrl, segmentBase, null); + return newInstance(revisionId, format, baseUrl, segmentBase, /* inbandEventStreams= */ null); } /** @@ -89,8 +88,9 @@ public abstract class Representation { Format format, String baseUrl, SegmentBase segmentBase, - List inbandEventStreams) { - return newInstance(revisionId, format, baseUrl, segmentBase, inbandEventStreams, null); + @Nullable List inbandEventStreams) { + return newInstance( + revisionId, format, baseUrl, segmentBase, inbandEventStreams, /* cacheKey= */ null); } /** @@ -110,8 +110,8 @@ public abstract class Representation { Format format, String baseUrl, SegmentBase segmentBase, - List inbandEventStreams, - String cacheKey) { + @Nullable List inbandEventStreams, + @Nullable String cacheKey) { if (segmentBase instanceof SingleSegmentBase) { return new SingleSegmentRepresentation( revisionId, @@ -135,7 +135,7 @@ public abstract class Representation { Format format, String baseUrl, SegmentBase segmentBase, - List inbandEventStreams) { + @Nullable List inbandEventStreams) { this.revisionId = revisionId; this.format = format; this.baseUrl = baseUrl; @@ -151,6 +151,7 @@ public abstract class Representation { * Returns a {@link RangedUri} defining the location of the representation's initialization data, * or null if no initialization data exists. */ + @Nullable public RangedUri getInitializationUri() { return initializationUri; } @@ -159,14 +160,15 @@ public abstract class Representation { * Returns a {@link RangedUri} defining the location of the representation's segment index, or * null if the representation provides an index directly. */ + @Nullable public abstract RangedUri getIndexUri(); - /** - * Returns an index if the representation provides one directly, or null otherwise. - */ + /** Returns an index if the representation provides one directly, or null otherwise. */ + @Nullable public abstract DashSegmentIndex getIndex(); /** Returns a cache key for the representation if set, or null. */ + @Nullable public abstract String getCacheKey(); /** @@ -184,9 +186,9 @@ public abstract class Representation { */ public final long contentLength; - private final String cacheKey; - private final RangedUri indexUri; - private final SingleSegmentIndex segmentIndex; + @Nullable private final String cacheKey; + @Nullable private final RangedUri indexUri; + @Nullable private final SingleSegmentIndex segmentIndex; /** * @param revisionId Identifies the revision of the content. @@ -209,7 +211,7 @@ public abstract class Representation { long indexStart, long indexEnd, List inbandEventStreams, - String cacheKey, + @Nullable String cacheKey, long contentLength) { RangedUri rangedUri = new RangedUri(null, initializationStart, initializationEnd - initializationStart + 1); @@ -233,8 +235,8 @@ public abstract class Representation { Format format, String baseUrl, SingleSegmentBase segmentBase, - List inbandEventStreams, - String cacheKey, + @Nullable List inbandEventStreams, + @Nullable String cacheKey, long contentLength) { super(revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.uri = Uri.parse(baseUrl); @@ -248,16 +250,19 @@ public abstract class Representation { } @Override + @Nullable public RangedUri getIndexUri() { return indexUri; } @Override + @Nullable public DashSegmentIndex getIndex() { return segmentIndex; } @Override + @Nullable public String getCacheKey() { return cacheKey; } @@ -284,12 +289,13 @@ public abstract class Representation { Format format, String baseUrl, MultiSegmentBase segmentBase, - List inbandEventStreams) { + @Nullable List inbandEventStreams) { super(revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.segmentBase = segmentBase; } @Override + @Nullable public RangedUri getIndexUri() { return null; } @@ -300,6 +306,7 @@ public abstract class Representation { } @Override + @Nullable public String getCacheKey() { return null; } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java index ba4faafd95..a31e0329af 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.dash.manifest; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.dash.DashSegmentIndex; import com.google.android.exoplayer2.util.Util; @@ -25,7 +26,7 @@ import java.util.List; */ public abstract class SegmentBase { - /* package */ final RangedUri initialization; + /* package */ @Nullable final RangedUri initialization; /* package */ final long timescale; /* package */ final long presentationTimeOffset; @@ -36,7 +37,8 @@ public abstract class SegmentBase { * @param presentationTimeOffset The presentation time offset. The value in seconds is the * division of this value and {@code timescale}. */ - public SegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset) { + public SegmentBase( + @Nullable RangedUri initialization, long timescale, long presentationTimeOffset) { this.initialization = initialization; this.timescale = timescale; this.presentationTimeOffset = presentationTimeOffset; @@ -49,6 +51,7 @@ public abstract class SegmentBase { * @param representation The {@link Representation} for which initialization data is required. * @return A {@link RangedUri} defining the location of the initialization data, or null. */ + @Nullable public RangedUri getInitialization(Representation representation) { return initialization; } @@ -77,19 +80,31 @@ public abstract class SegmentBase { * @param indexStart The byte offset of the index data in the segment. * @param indexLength The length of the index data in bytes. */ - public SingleSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset, - long indexStart, long indexLength) { + public SingleSegmentBase( + @Nullable RangedUri initialization, + long timescale, + long presentationTimeOffset, + long indexStart, + long indexLength) { super(initialization, timescale, presentationTimeOffset); this.indexStart = indexStart; this.indexLength = indexLength; } public SingleSegmentBase() { - this(null, 1, 0, 0, 0); + this( + /* initialization= */ null, + /* timescale= */ 1, + /* presentationTimeOffset= */ 0, + /* indexStart= */ 0, + /* indexLength= */ 0); } + @Nullable public RangedUri getIndex() { - return indexLength <= 0 ? null : new RangedUri(null, indexStart, indexLength); + return indexLength <= 0 + ? null + : new RangedUri(/* referenceUri= */ null, indexStart, indexLength); } } @@ -101,7 +116,7 @@ public abstract class SegmentBase { /* package */ final long startNumber; /* package */ final long duration; - /* package */ final List segmentTimeline; + /* package */ @Nullable final List segmentTimeline; /** * @param initialization A {@link RangedUri} corresponding to initialization data, if such data @@ -118,12 +133,12 @@ public abstract class SegmentBase { * parameter. */ public MultiSegmentBase( - RangedUri initialization, + @Nullable RangedUri initialization, long timescale, long presentationTimeOffset, long startNumber, long duration, - List segmentTimeline) { + @Nullable List segmentTimeline) { super(initialization, timescale, presentationTimeOffset); this.startNumber = startNumber; this.duration = duration; @@ -223,7 +238,7 @@ public abstract class SegmentBase { */ public static class SegmentList extends MultiSegmentBase { - /* package */ final List mediaSegments; + /* package */ @Nullable final List mediaSegments; /** * @param initialization A {@link RangedUri} corresponding to initialization data, if such data @@ -246,8 +261,8 @@ public abstract class SegmentBase { long presentationTimeOffset, long startNumber, long duration, - List segmentTimeline, - List mediaSegments) { + @Nullable List segmentTimeline, + @Nullable List mediaSegments) { super(initialization, timescale, presentationTimeOffset, startNumber, duration, segmentTimeline); this.mediaSegments = mediaSegments; @@ -275,8 +290,8 @@ public abstract class SegmentBase { */ public static class SegmentTemplate extends MultiSegmentBase { - /* package */ final UrlTemplate initializationTemplate; - /* package */ final UrlTemplate mediaTemplate; + /* package */ @Nullable final UrlTemplate initializationTemplate; + /* package */ @Nullable final UrlTemplate mediaTemplate; /* package */ final long endNumber; /** @@ -308,9 +323,9 @@ public abstract class SegmentBase { long startNumber, long endNumber, long duration, - List segmentTimeline, - UrlTemplate initializationTemplate, - UrlTemplate mediaTemplate) { + @Nullable List segmentTimeline, + @Nullable UrlTemplate initializationTemplate, + @Nullable UrlTemplate mediaTemplate) { super( initialization, timescale, @@ -324,6 +339,7 @@ public abstract class SegmentBase { } @Override + @Nullable public RangedUri getInitialization(Representation representation) { if (initializationTemplate != null) { String urlString = initializationTemplate.buildUri(representation.format.id, 0, diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/package-info.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/package-info.java new file mode 100644 index 0000000000..b7c267727c --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.source.dash.manifest; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/package-info.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/package-info.java new file mode 100644 index 0000000000..4eb0d8436d --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.source.dash.offline; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/package-info.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/package-info.java new file mode 100644 index 0000000000..f51ea4369e --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.source.dash; + +import com.google.android.exoplayer2.util.NonNullApi; From 58d4fd93dd07d9ad7af8b76eed111be5550bb0d1 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 7 Aug 2019 14:42:47 +0100 Subject: [PATCH 1548/1556] Fix HLS module API nullability issues and add package-level non-null-by-default PiperOrigin-RevId: 262124441 --- library/hls/build.gradle | 1 + .../source/hls/Aes128DataSource.java | 3 ++- .../hls/DefaultHlsExtractorFactory.java | 6 +++--- .../exoplayer2/source/hls/HlsChunkSource.java | 10 ++++------ .../source/hls/HlsExtractorFactory.java | 7 ++++--- .../exoplayer2/source/hls/HlsMediaPeriod.java | 9 +++++++-- .../exoplayer2/source/hls/HlsMediaSource.java | 2 +- .../source/hls/WebvttExtractor.java | 5 +++-- .../source/hls/offline/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/source/hls/package-info.java | 19 +++++++++++++++++++ .../playlist/DefaultHlsPlaylistTracker.java | 4 +++- .../hls/playlist/HlsMasterPlaylist.java | 4 ++-- .../source/hls/playlist/HlsMediaPlaylist.java | 7 +++---- .../source/hls/playlist/package-info.java | 19 +++++++++++++++++++ 14 files changed, 90 insertions(+), 25 deletions(-) create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/package-info.java create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/package-info.java create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/package-info.java diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 82e09ab72c..8301820e79 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -41,6 +41,7 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion implementation project(modulePrefix + 'library-core') testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java index 022d62cbfc..fe70298dc8 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java @@ -105,7 +105,8 @@ import javax.crypto.spec.SecretKeySpec; } @Override - public final @Nullable Uri getUri() { + @Nullable + public final Uri getUri() { return upstream.getUri(); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java index 9fde54a705..6dd4ade590 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -84,11 +84,11 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { @Override public Result createExtractor( - Extractor previousExtractor, + @Nullable Extractor previousExtractor, Uri uri, Format format, - List muxedCaptionFormats, - DrmInitData drmInitData, + @Nullable List muxedCaptionFormats, + @Nullable DrmInitData drmInitData, TimestampAdjuster timestampAdjuster, Map> responseHeaders, ExtractorInput extractorInput) 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 ee5a5f0809..c452a29cf9 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 @@ -59,10 +59,8 @@ import java.util.Map; clear(); } - /** - * The chunk to be loaded next. - */ - public Chunk chunk; + /** The chunk to be loaded next. */ + @Nullable public Chunk chunk; /** * Indicates that the end of the stream has been reached. @@ -70,7 +68,7 @@ import java.util.Map; public boolean endOfStream; /** Indicates that the chunk source is waiting for the referred playlist to be refreshed. */ - public Uri playlistUrl; + @Nullable public Uri playlistUrl; /** * Clears the holder. @@ -138,7 +136,7 @@ import java.util.Map; HlsDataSourceFactory dataSourceFactory, @Nullable TransferListener mediaTransferListener, TimestampAdjusterProvider timestampAdjusterProvider, - List muxedCaptionFormats) { + @Nullable List muxedCaptionFormats) { this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; this.playlistUrls = playlistUrls; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java index 103d89188f..927b79899d 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.Extractor; @@ -82,11 +83,11 @@ public interface HlsExtractorFactory { * @throws IOException If an I/O error is encountered while sniffing. */ Result createExtractor( - Extractor previousExtractor, + @Nullable Extractor previousExtractor, Uri uri, Format format, - List muxedCaptionFormats, - DrmInitData drmInitData, + @Nullable List muxedCaptionFormats, + @Nullable DrmInitData drmInitData, TimestampAdjuster timestampAdjuster, Map> responseHeaders, ExtractorInput sniffingExtractorInput) 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 e21827557a..8053958c2b 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 @@ -54,6 +54,7 @@ import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A {@link MediaPeriod} that loads an HLS stream. @@ -249,8 +250,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { // Map each selection and stream onto a child period index. int[] streamChildIndices = new int[selections.length]; int[] selectionChildIndices = new int[selections.length]; 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 877b6d486e..f2db9541eb 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 @@ -110,7 +110,7 @@ public final class HlsMediaSource extends BaseMediaSource * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setTag(Object tag) { + public Factory setTag(@Nullable Object tag) { Assertions.checkState(!isCreateCalled); this.tag = tag; return this; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java index 665f2e0570..a89e907a37 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.hls; +import androidx.annotation.Nullable; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -49,7 +50,7 @@ public final class WebvttExtractor implements Extractor { private static final int HEADER_MIN_LENGTH = 6 /* "WEBVTT" */; private static final int HEADER_MAX_LENGTH = 3 /* optional Byte Order Mark */ + HEADER_MIN_LENGTH; - private final String language; + @Nullable private final String language; private final TimestampAdjuster timestampAdjuster; private final ParsableByteArray sampleDataWrapper; @@ -58,7 +59,7 @@ public final class WebvttExtractor implements Extractor { private byte[] sampleData; private int sampleSize; - public WebvttExtractor(String language, TimestampAdjuster timestampAdjuster) { + public WebvttExtractor(@Nullable String language, TimestampAdjuster timestampAdjuster) { this.language = language; this.timestampAdjuster = timestampAdjuster; this.sampleDataWrapper = new ParsableByteArray(); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/package-info.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/package-info.java new file mode 100644 index 0000000000..2527553824 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.source.hls.offline; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/package-info.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/package-info.java new file mode 100644 index 0000000000..55f15f5e7a --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.source.hls; + +import com.google.android.exoplayer2.util.NonNullApi; 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 a4fd28009f..e7a072839e 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 @@ -172,6 +172,7 @@ public final class DefaultHlsPlaylistTracker } @Override + @Nullable public HlsMediaPlaylist getPlaylistSnapshot(Uri url, boolean isForPlayback) { HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot(); if (snapshot != null && isForPlayback) { @@ -448,7 +449,7 @@ public final class DefaultHlsPlaylistTracker private final Loader mediaPlaylistLoader; private final ParsingLoadable mediaPlaylistLoadable; - private HlsMediaPlaylist playlistSnapshot; + @Nullable private HlsMediaPlaylist playlistSnapshot; private long lastSnapshotLoadMs; private long lastSnapshotChangeMs; private long earliestNextLoadTimeMs; @@ -467,6 +468,7 @@ public final class DefaultHlsPlaylistTracker mediaPlaylistParser); } + @Nullable public HlsMediaPlaylist getPlaylistSnapshot() { return playlistSnapshot; } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index 0e86df8c2f..1660324a34 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -174,7 +174,7 @@ public final class HlsMasterPlaylist extends HlsPlaylist { * The format of the audio muxed in the variants. May be null if the playlist does not declare any * muxed audio. */ - public final Format muxedAudioFormat; + @Nullable public final Format muxedAudioFormat; /** * The format of the closed captions declared by the playlist. May be empty if the playlist * explicitly declares no captions are available, or null if the playlist does not declare any @@ -208,7 +208,7 @@ public final class HlsMasterPlaylist extends HlsPlaylist { List audios, List subtitles, List closedCaptions, - Format muxedAudioFormat, + @Nullable Format muxedAudioFormat, List muxedCaptionFormats, boolean hasIndependentSegments, Map variableDefinitions, diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 4411c9865e..58f500cf94 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source.hls.playlist; import androidx.annotation.IntDef; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData; @@ -95,8 +94,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist { String uri, long byterangeOffset, long byterangeLength, - String fullSegmentEncryptionKeyUri, - String encryptionIV) { + @Nullable String fullSegmentEncryptionKeyUri, + @Nullable String encryptionIV) { this( uri, /* initializationSegment= */ null, @@ -154,7 +153,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } @Override - public int compareTo(@NonNull Long relativeStartTimeUs) { + public int compareTo(Long relativeStartTimeUs) { return this.relativeStartTimeUs > relativeStartTimeUs ? 1 : (this.relativeStartTimeUs < relativeStartTimeUs ? -1 : 0); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/package-info.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/package-info.java new file mode 100644 index 0000000000..61f9d77e72 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.source.hls.playlist; + +import com.google.android.exoplayer2.util.NonNullApi; From 79d627d441bc2f49cc1f7832a959085907953c70 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 7 Aug 2019 15:56:25 +0100 Subject: [PATCH 1549/1556] Simplify EventMessageEncoder/Decoder serialization We're no longer tied to the emsg spec, so we can skip unused fields and assume ms for duration. Also remove @Nullable annotation from EventMessageEncoder#encode, it seems the current implementation never returns null PiperOrigin-RevId: 262135009 --- .../metadata/emsg/EventMessageDecoder.java | 15 +--- .../metadata/emsg/EventMessageEncoder.java | 4 -- .../emsg/EventMessageDecoderTest.java | 19 ++--- .../emsg/EventMessageEncoderTest.java | 69 ++++++++----------- 4 files changed, 40 insertions(+), 67 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index 340b662e97..f592a6eee7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -15,22 +15,17 @@ */ package com.google.android.exoplayer2.metadata.emsg; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.util.Arrays; /** Decodes data encoded by {@link EventMessageEncoder}. */ public final class EventMessageDecoder implements MetadataDecoder { - private static final String TAG = "EventMessageDecoder"; - @SuppressWarnings("ByteBufferBackingArray") @Override public Metadata decode(MetadataInputBuffer inputBuffer) { @@ -43,15 +38,7 @@ public final class EventMessageDecoder implements MetadataDecoder { public EventMessage decode(ParsableByteArray emsgData) { String schemeIdUri = Assertions.checkNotNull(emsgData.readNullTerminatedString()); String value = Assertions.checkNotNull(emsgData.readNullTerminatedString()); - long timescale = emsgData.readUnsignedInt(); - long presentationTimeDelta = emsgData.readUnsignedInt(); - if (presentationTimeDelta != 0) { - // We expect the source to have accounted for presentation_time_delta by adjusting the sample - // timestamp and zeroing the field in the sample data. Log a warning if the field is non-zero. - Log.w(TAG, "Ignoring non-zero presentation_time_delta: " + presentationTimeDelta); - } - long durationMs = - Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); + long durationMs = emsgData.readUnsignedInt(); long id = emsgData.readUnsignedInt(); byte[] messageData = Arrays.copyOfRange(emsgData.data, emsgData.getPosition(), emsgData.limit()); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java index dd33d591a7..4fa3f71b32 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.metadata.emsg; -import androidx.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; @@ -40,15 +39,12 @@ public final class EventMessageEncoder { * @param eventMessage The event message to be encoded. * @return The serialized byte array. */ - @Nullable public byte[] encode(EventMessage eventMessage) { byteArrayOutputStream.reset(); try { writeNullTerminatedString(dataOutputStream, eventMessage.schemeIdUri); String nonNullValue = eventMessage.value != null ? eventMessage.value : ""; writeNullTerminatedString(dataOutputStream, nonNullValue); - writeUnsignedInt(dataOutputStream, 1000); // timescale - writeUnsignedInt(dataOutputStream, 0); // presentation_time_delta writeUnsignedInt(dataOutputStream, eventMessage.durationMs); writeUnsignedInt(dataOutputStream, eventMessage.id); dataOutputStream.write(eventMessage.messageData); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java index d870afac3a..88a61d0bce 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata.emsg; +import static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; +import static com.google.android.exoplayer2.testutil.TestUtil.joinByteArrays; import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -30,18 +32,19 @@ public final class EventMessageDecoderTest { @Test public void testDecodeEventMessage() { - byte[] rawEmsgBody = new byte[] { - 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" - 49, 50, 51, 0, // value = "123" - 0, 0, -69, -128, // timescale = 48000 - 0, 0, -69, -128, // presentation_time_delta = 48000 - 0, 2, 50, -128, // event_duration = 144000 - 0, 15, 67, -45, // id = 1000403 - 0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4} + byte[] rawEmsgBody = + joinByteArrays( + createByteArray(117, 114, 110, 58, 116, 101, 115, 116, 0), // scheme_id_uri = "urn:test" + createByteArray(49, 50, 51, 0), // value = "123" + createByteArray(0, 0, 11, 184), // event_duration_ms = 3000 + createByteArray(0, 15, 67, 211), // id = 1000403 + createByteArray(0, 1, 2, 3, 4)); // message_data = {0, 1, 2, 3, 4} EventMessageDecoder decoder = new EventMessageDecoder(); MetadataInputBuffer buffer = new MetadataInputBuffer(); buffer.data = ByteBuffer.allocate(rawEmsgBody.length).put(rawEmsgBody); + Metadata metadata = decoder.decode(buffer); + assertThat(metadata.length()).isEqualTo(1); EventMessage eventMessage = (EventMessage) metadata.get(0); assertThat(eventMessage.schemeIdUri).isEqualTo("urn:test"); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java index ca8303d3e2..56830035cc 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata.emsg; +import static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; +import static com.google.android.exoplayer2.testutil.TestUtil.joinByteArrays; import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -29,67 +31,52 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class EventMessageEncoderTest { + private static final EventMessage DECODED_MESSAGE = + new EventMessage("urn:test", "123", 3000, 1000403, new byte[] {0, 1, 2, 3, 4}); + + private static final byte[] ENCODED_MESSAGE = + joinByteArrays( + createByteArray(117, 114, 110, 58, 116, 101, 115, 116, 0), // scheme_id_uri = "urn:test" + createByteArray(49, 50, 51, 0), // value = "123" + createByteArray(0, 0, 11, 184), // event_duration_ms = 3000 + createByteArray(0, 15, 67, 211), // id = 1000403 + createByteArray(0, 1, 2, 3, 4)); // message_data = {0, 1, 2, 3, 4} + @Test public void testEncodeEventStream() throws IOException { - EventMessage eventMessage = - new EventMessage("urn:test", "123", 3000, 1000403, new byte[] {0, 1, 2, 3, 4}); - byte[] expectedEmsgBody = - new byte[] { - 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" - 49, 50, 51, 0, // value = "123" - 0, 0, 3, -24, // timescale = 1000 - 0, 0, 0, 0, // presentation_time_delta = 0 - 0, 0, 11, -72, // event_duration = 3000 - 0, 15, 67, -45, // id = 1000403 - 0, 1, 2, 3, 4 - }; // message_data = {0, 1, 2, 3, 4} - byte[] encodedByteArray = new EventMessageEncoder().encode(eventMessage); - assertThat(encodedByteArray).isEqualTo(expectedEmsgBody); + byte[] foo = new byte[] {1, 2, 3}; + + byte[] encodedByteArray = new EventMessageEncoder().encode(DECODED_MESSAGE); + assertThat(encodedByteArray).isEqualTo(ENCODED_MESSAGE); } @Test public void testEncodeDecodeEventStream() throws IOException { - EventMessage expectedEmsg = - new EventMessage("urn:test", "123", 3000, 1000403, new byte[] {0, 1, 2, 3, 4}); - byte[] encodedByteArray = new EventMessageEncoder().encode(expectedEmsg); + byte[] encodedByteArray = new EventMessageEncoder().encode(DECODED_MESSAGE); MetadataInputBuffer buffer = new MetadataInputBuffer(); buffer.data = ByteBuffer.allocate(encodedByteArray.length).put(encodedByteArray); EventMessageDecoder decoder = new EventMessageDecoder(); Metadata metadata = decoder.decode(buffer); assertThat(metadata.length()).isEqualTo(1); - assertThat(metadata.get(0)).isEqualTo(expectedEmsg); + assertThat(metadata.get(0)).isEqualTo(DECODED_MESSAGE); } @Test public void testEncodeEventStreamMultipleTimesWorkingCorrectly() throws IOException { - EventMessage eventMessage = - new EventMessage("urn:test", "123", 3000, 1000403, new byte[] {0, 1, 2, 3, 4}); - byte[] expectedEmsgBody = - new byte[] { - 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" - 49, 50, 51, 0, // value = "123" - 0, 0, 3, -24, // timescale = 1000 - 0, 0, 0, 0, // presentation_time_delta = 0 - 0, 0, 11, -72, // event_duration = 3000 - 0, 15, 67, -45, // id = 1000403 - 0, 1, 2, 3, 4 - }; // message_data = {0, 1, 2, 3, 4} EventMessage eventMessage1 = new EventMessage("urn:test", "123", 3000, 1000402, new byte[] {4, 3, 2, 1, 0}); byte[] expectedEmsgBody1 = - new byte[] { - 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" - 49, 50, 51, 0, // value = "123" - 0, 0, 3, -24, // timescale = 1000 - 0, 0, 0, 0, // presentation_time_delta = 0 - 0, 0, 11, -72, // event_duration = 3000 - 0, 15, 67, -46, // id = 1000402 - 4, 3, 2, 1, 0 - }; // message_data = {4, 3, 2, 1, 0} + joinByteArrays( + createByteArray(117, 114, 110, 58, 116, 101, 115, 116, 0), // scheme_id_uri = "urn:test" + createByteArray(49, 50, 51, 0), // value = "123" + createByteArray(0, 0, 11, 184), // event_duration_ms = 3000 + createByteArray(0, 15, 67, 210), // id = 1000402 + createByteArray(4, 3, 2, 1, 0)); // message_data = {4, 3, 2, 1, 0} + EventMessageEncoder eventMessageEncoder = new EventMessageEncoder(); - byte[] encodedByteArray = eventMessageEncoder.encode(eventMessage); - assertThat(encodedByteArray).isEqualTo(expectedEmsgBody); + byte[] encodedByteArray = eventMessageEncoder.encode(DECODED_MESSAGE); + assertThat(encodedByteArray).isEqualTo(ENCODED_MESSAGE); byte[] encodedByteArray1 = eventMessageEncoder.encode(eventMessage1); assertThat(encodedByteArray1).isEqualTo(expectedEmsgBody1); } From 70b912c23e445ce89aa8932a88ad7d149fcd52d2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 8 Aug 2019 09:22:26 +0100 Subject: [PATCH 1550/1556] Fix API nullability of remaining extensions and mark them as non-null-by-default PiperOrigin-RevId: 262303610 --- .../ext/vp9/LibvpxVideoRenderer.java | 16 +++++++-------- .../exoplayer2/ext/vp9/VpxDecoder.java | 20 ++++++++++++++----- .../ext/vp9/VpxVideoSurfaceView.java | 5 +++-- .../exoplayer2/ext/vp9/package-info.java | 19 ++++++++++++++++++ 4 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/package-info.java diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index b6663ac3d7..5e9d8d0897 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -174,8 +174,8 @@ public class LibvpxVideoRenderer extends BaseRenderer { */ public LibvpxVideoRenderer( long allowedJoiningTimeMs, - Handler eventHandler, - VideoRendererEventListener eventListener, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) { this( allowedJoiningTimeMs, @@ -206,10 +206,10 @@ public class LibvpxVideoRenderer extends BaseRenderer { */ public LibvpxVideoRenderer( long allowedJoiningTimeMs, - Handler eventHandler, - VideoRendererEventListener eventListener, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify, - DrmSessionManager drmSessionManager, + @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, boolean disableLoopFilter) { this( @@ -249,10 +249,10 @@ public class LibvpxVideoRenderer extends BaseRenderer { */ public LibvpxVideoRenderer( long allowedJoiningTimeMs, - Handler eventHandler, - VideoRendererEventListener eventListener, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify, - DrmSessionManager drmSessionManager, + @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, boolean disableLoopFilter, boolean enableRowMultiThreadMode, diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 93a4a2fc1f..0efd4bd0ea 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -33,7 +33,7 @@ import java.nio.ByteBuffer; private static final int DECODE_ERROR = 1; private static final int DRM_ERROR = 2; - private final ExoMediaCrypto exoMediaCrypto; + @Nullable private final ExoMediaCrypto exoMediaCrypto; private final long vpxDecContext; @C.VideoOutputMode private volatile int outputMode; @@ -55,7 +55,7 @@ import java.nio.ByteBuffer; int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, - ExoMediaCrypto exoMediaCrypto, + @Nullable ExoMediaCrypto exoMediaCrypto, boolean disableLoopFilter, boolean enableRowMultiThreadMode, int threads) @@ -170,9 +170,19 @@ import java.nio.ByteBuffer; private native long vpxClose(long context); private native long vpxDecode(long context, ByteBuffer encoded, int length); - private native long vpxSecureDecode(long context, ByteBuffer encoded, int length, - ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv, - int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData); + + private native long vpxSecureDecode( + long context, + ByteBuffer encoded, + int length, + @Nullable ExoMediaCrypto mediaCrypto, + int inputMode, + byte[] key, + byte[] iv, + int numSubSamples, + int[] numBytesOfClearData, + int[] numBytesOfEncryptedData); + private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer); /** diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java index 8c765952e7..4e983cccc7 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.vp9; import android.content.Context; import android.opengl.GLSurfaceView; +import androidx.annotation.Nullable; import android.util.AttributeSet; /** @@ -27,10 +28,10 @@ public class VpxVideoSurfaceView extends GLSurfaceView implements VpxOutputBuffe private final VpxRenderer renderer; public VpxVideoSurfaceView(Context context) { - this(context, null); + this(context, /* attrs= */ null); } - public VpxVideoSurfaceView(Context context, AttributeSet attrs) { + public VpxVideoSurfaceView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); renderer = new VpxRenderer(); setPreserveEGLContextOnPause(true); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/package-info.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/package-info.java new file mode 100644 index 0000000000..b8725607a5 --- /dev/null +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.ext.vp9; + +import com.google.android.exoplayer2.util.NonNullApi; From 313bd109517351e758f5c0a0085b9d780a459e6e Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 8 Aug 2019 09:44:58 +0100 Subject: [PATCH 1551/1556] Fix SS module API nullability issues and add package-level non-null-by-default PiperOrigin-RevId: 262306255 --- library/smoothstreaming/build.gradle | 1 + .../source/smoothstreaming/SsMediaPeriod.java | 9 +++- .../source/smoothstreaming/SsMediaSource.java | 30 ++++++------ .../smoothstreaming/manifest/SsManifest.java | 46 ++++++++++++++----- .../manifest/SsManifestParser.java | 21 +++++---- .../manifest/package-info.java | 19 ++++++++ .../smoothstreaming/offline/package-info.java | 19 ++++++++ .../source/smoothstreaming/package-info.java | 19 ++++++++ 8 files changed, 127 insertions(+), 37 deletions(-) create mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/package-info.java create mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/package-info.java create mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/package-info.java diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index fa67ea1d01..d85ecbb1a3 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -41,6 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') } 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 d103358d37..b3d950959a 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 @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.upstream.TransferListener; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** A SmoothStreaming {@link MediaPeriod}. */ /* package */ final class SsMediaPeriod @@ -120,8 +121,12 @@ import java.util.List; } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { ArrayList> sampleStreamsList = new ArrayList<>(); for (int i = 0; i < selections.length; i++) { if (streams[i] != null) { diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 3c0593200e..9ddc7aa0f0 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -115,7 +115,7 @@ public final class SsMediaSource extends BaseMediaSource * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setTag(Object tag) { + public Factory setTag(@Nullable Object tag) { Assertions.checkState(!isCreateCalled); this.tag = tag; return this; @@ -370,8 +370,8 @@ public final class SsMediaSource extends BaseMediaSource public SsMediaSource( SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifest, chunkSourceFactory, @@ -395,8 +395,8 @@ public final class SsMediaSource extends BaseMediaSource SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifest, /* manifestUri= */ null, @@ -431,8 +431,8 @@ public final class SsMediaSource extends BaseMediaSource Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifestUri, manifestDataSourceFactory, @@ -466,8 +466,8 @@ public final class SsMediaSource extends BaseMediaSource SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, new SsManifestParser(), chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -496,8 +496,8 @@ public final class SsMediaSource extends BaseMediaSource SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( /* manifest= */ null, manifestUri, @@ -515,10 +515,10 @@ public final class SsMediaSource extends BaseMediaSource } private SsMediaSource( - SsManifest manifest, - Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, - ParsingLoadable.Parser manifestParser, + @Nullable SsManifest manifest, + @Nullable Uri manifestUri, + @Nullable DataSource.Factory manifestDataSourceFactory, + @Nullable ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, DrmSessionManager drmSessionManager, diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java index cfb772a86b..b91bfc8f67 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.smoothstreaming.manifest; import android.net.Uri; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; @@ -69,7 +70,7 @@ public class SsManifest implements FilterableManifest { public final int maxHeight; public final int displayWidth; public final int displayHeight; - public final String language; + @Nullable public final String language; public final Format[] formats; public final int chunkCount; @@ -80,9 +81,20 @@ public class SsManifest implements FilterableManifest { private final long[] chunkStartTimesUs; private final long lastChunkDurationUs; - public StreamElement(String baseUri, String chunkTemplate, int type, String subType, - long timescale, String name, int maxWidth, int maxHeight, int displayWidth, - int displayHeight, String language, Format[] formats, List chunkStartTimes, + public StreamElement( + String baseUri, + String chunkTemplate, + int type, + String subType, + long timescale, + String name, + int maxWidth, + int maxHeight, + int displayWidth, + int displayHeight, + @Nullable String language, + Format[] formats, + List chunkStartTimes, long lastChunkDuration) { this( baseUri, @@ -102,10 +114,22 @@ public class SsManifest implements FilterableManifest { Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale)); } - private StreamElement(String baseUri, String chunkTemplate, int type, String subType, - long timescale, String name, int maxWidth, int maxHeight, int displayWidth, - int displayHeight, String language, Format[] formats, List chunkStartTimes, - long[] chunkStartTimesUs, long lastChunkDurationUs) { + private StreamElement( + String baseUri, + String chunkTemplate, + int type, + String subType, + long timescale, + String name, + int maxWidth, + int maxHeight, + int displayWidth, + int displayHeight, + @Nullable String language, + Format[] formats, + List chunkStartTimes, + long[] chunkStartTimesUs, + long lastChunkDurationUs) { this.baseUri = baseUri; this.chunkTemplate = chunkTemplate; this.type = type; @@ -208,7 +232,7 @@ public class SsManifest implements FilterableManifest { public final boolean isLive; /** Content protection information, or null if the content is not protected. */ - public final ProtectionElement protectionElement; + @Nullable public final ProtectionElement protectionElement; /** The contained stream elements. */ public final StreamElement[] streamElements; @@ -249,7 +273,7 @@ public class SsManifest implements FilterableManifest { long dvrWindowLength, int lookAheadCount, boolean isLive, - ProtectionElement protectionElement, + @Nullable ProtectionElement protectionElement, StreamElement[] streamElements) { this( majorVersion, @@ -273,7 +297,7 @@ public class SsManifest implements FilterableManifest { long dvrWindowLengthUs, int lookAheadCount, boolean isLive, - ProtectionElement protectionElement, + @Nullable ProtectionElement protectionElement, StreamElement[] streamElements) { this.majorVersion = majorVersion; this.minorVersion = minorVersion; diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java index 39e22f2982..03e9e91e22 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.smoothstreaming.manifest; import android.net.Uri; +import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Base64; import android.util.Pair; @@ -40,6 +41,7 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.UUID; +import org.checkerframework.checker.nullness.compatqual.NullableType; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; @@ -94,10 +96,10 @@ public class SsManifestParser implements ParsingLoadable.Parser { private final String baseUri; private final String tag; - private final ElementParser parent; - private final List> normalizedAttributes; + @Nullable private final ElementParser parent; + private final List> normalizedAttributes; - public ElementParser(ElementParser parent, String baseUri, String tag) { + public ElementParser(@Nullable ElementParser parent, String baseUri, String tag) { this.parent = parent; this.baseUri = baseUri; this.tag = tag; @@ -174,24 +176,25 @@ public class SsManifestParser implements ParsingLoadable.Parser { * Stash an attribute that may be normalized at this level. In other words, an attribute that * may have been pulled up from the child elements because its value was the same in all * children. - *

          - * Stashing an attribute allows child element parsers to retrieve the values of normalized + * + *

          Stashing an attribute allows child element parsers to retrieve the values of normalized * attributes using {@link #getNormalizedAttribute(String)}. * * @param key The name of the attribute. * @param value The value of the attribute. */ - protected final void putNormalizedAttribute(String key, Object value) { + protected final void putNormalizedAttribute(String key, @Nullable Object value) { normalizedAttributes.add(Pair.create(key, value)); } /** - * Attempt to retrieve a stashed normalized attribute. If there is no stashed attribute with - * the provided name, the parent element parser will be queried, and so on up the chain. + * Attempt to retrieve a stashed normalized attribute. If there is no stashed attribute with the + * provided name, the parent element parser will be queried, and so on up the chain. * * @param key The name of the attribute. * @return The stashed value, or null if the attribute was not be found. */ + @Nullable protected final Object getNormalizedAttribute(String key) { for (int i = 0; i < normalizedAttributes.size(); i++) { Pair pair = normalizedAttributes.get(i); @@ -340,7 +343,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { private long dvrWindowLength; private int lookAheadCount; private boolean isLive; - private ProtectionElement protectionElement; + @Nullable private ProtectionElement protectionElement; public SmoothStreamingMediaParser(ElementParser parent, String baseUri) { super(parent, baseUri, TAG); diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/package-info.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/package-info.java new file mode 100644 index 0000000000..b594ddc2bc --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.source.smoothstreaming.manifest; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/package-info.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/package-info.java new file mode 100644 index 0000000000..f7c74f1a1e --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.source.smoothstreaming.offline; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/package-info.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/package-info.java new file mode 100644 index 0000000000..23e85850c6 --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.source.smoothstreaming; + +import com.google.android.exoplayer2.util.NonNullApi; From bbe681a904d58fe1f84f6c7c6e3390d932c86249 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 8 Aug 2019 11:09:33 +0100 Subject: [PATCH 1552/1556] Make Kotlin JVM annotations available and use in ExoPlayer. NoExternal PiperOrigin-RevId: 262316962 --- constants.gradle | 1 + library/core/build.gradle | 3 +-- library/core/proguard-rules.txt | 3 ++- .../com/google/android/exoplayer2/util/NonNullApi.java | 8 +++----- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/constants.gradle b/constants.gradle index b1c2c636c7..9510b8442e 100644 --- a/constants.gradle +++ b/constants.gradle @@ -25,6 +25,7 @@ project.ext { autoServiceVersion = '1.0-rc4' checkerframeworkVersion = '2.5.0' jsr305Version = '3.0.2' + kotlinAnnotationsVersion = '1.3.31' androidXTestVersion = '1.1.0' truthVersion = '0.44' modulePrefix = ':' diff --git a/library/core/build.gradle b/library/core/build.gradle index 93126d9830..8e64383638 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -62,8 +62,7 @@ dependencies { compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion - // Uncomment to enable Kotlin non-null strict mode. See [internal: b/138703808]. - // compileOnly "org.jetbrains.kotlin:kotlin-annotations-jvm:1.1.60" + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index 1f7a8d0ee7..ab3cc5fccd 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -58,5 +58,6 @@ (com.google.android.exoplayer2.upstream.DataSource$Factory); } -# Don't warn about checkerframework +# Don't warn about checkerframework and Kotlin annotations -dontwarn org.checkerframework.** +-dontwarn kotlin.annotations.jvm.** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java b/library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java index bd7a70eba0..7678710f18 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java @@ -20,8 +20,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import javax.annotation.Nonnull; import javax.annotation.meta.TypeQualifierDefault; -// import kotlin.annotations.jvm.MigrationStatus; -// import kotlin.annotations.jvm.UnderMigration; +import kotlin.annotations.jvm.MigrationStatus; +import kotlin.annotations.jvm.UnderMigration; /** * Annotation to declare all type usages in the annotated instance as {@link Nonnull}, unless @@ -29,8 +29,6 @@ import javax.annotation.meta.TypeQualifierDefault; */ @Nonnull @TypeQualifierDefault(ElementType.TYPE_USE) -// TODO(internal: b/138703808): Uncomment to ensure Kotlin issues compiler errors when non-null -// types are used incorrectly. -// @UnderMigration(status = MigrationStatus.STRICT) +@UnderMigration(status = MigrationStatus.STRICT) @Retention(RetentionPolicy.CLASS) public @interface NonNullApi {} From 9f55045eeb07120d5c001db48ef7c7c622089cbd Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 8 Aug 2019 12:08:51 +0100 Subject: [PATCH 1553/1556] Rollback of https://github.com/google/ExoPlayer/commit/bbe681a904d58fe1f84f6c7c6e3390d932c86249 *** Original commit *** Make Kotlin JVM annotations available and use in ExoPlayer. NoExternal *** PiperOrigin-RevId: 262323737 --- constants.gradle | 1 - library/core/build.gradle | 3 ++- library/core/proguard-rules.txt | 3 +-- .../com/google/android/exoplayer2/util/NonNullApi.java | 8 +++++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/constants.gradle b/constants.gradle index 9510b8442e..b1c2c636c7 100644 --- a/constants.gradle +++ b/constants.gradle @@ -25,7 +25,6 @@ project.ext { autoServiceVersion = '1.0-rc4' checkerframeworkVersion = '2.5.0' jsr305Version = '3.0.2' - kotlinAnnotationsVersion = '1.3.31' androidXTestVersion = '1.1.0' truthVersion = '0.44' modulePrefix = ':' diff --git a/library/core/build.gradle b/library/core/build.gradle index 8e64383638..93126d9830 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -62,7 +62,8 @@ dependencies { compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion - compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion + // Uncomment to enable Kotlin non-null strict mode. See [internal: b/138703808]. + // compileOnly "org.jetbrains.kotlin:kotlin-annotations-jvm:1.1.60" androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index ab3cc5fccd..1f7a8d0ee7 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -58,6 +58,5 @@ (com.google.android.exoplayer2.upstream.DataSource$Factory); } -# Don't warn about checkerframework and Kotlin annotations +# Don't warn about checkerframework -dontwarn org.checkerframework.** --dontwarn kotlin.annotations.jvm.** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java b/library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java index 7678710f18..bd7a70eba0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java @@ -20,8 +20,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import javax.annotation.Nonnull; import javax.annotation.meta.TypeQualifierDefault; -import kotlin.annotations.jvm.MigrationStatus; -import kotlin.annotations.jvm.UnderMigration; +// import kotlin.annotations.jvm.MigrationStatus; +// import kotlin.annotations.jvm.UnderMigration; /** * Annotation to declare all type usages in the annotated instance as {@link Nonnull}, unless @@ -29,6 +29,8 @@ import kotlin.annotations.jvm.UnderMigration; */ @Nonnull @TypeQualifierDefault(ElementType.TYPE_USE) -@UnderMigration(status = MigrationStatus.STRICT) +// TODO(internal: b/138703808): Uncomment to ensure Kotlin issues compiler errors when non-null +// types are used incorrectly. +// @UnderMigration(status = MigrationStatus.STRICT) @Retention(RetentionPolicy.CLASS) public @interface NonNullApi {} From a14df33dc76d628b1f7c1d90fd58128a2dae442d Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 8 Aug 2019 16:56:57 +0100 Subject: [PATCH 1554/1556] Only read from FormatHolder when a format has been read I think we need to start clearing the holder as part of the DRM rework. When we do this, it'll only be valid to read from the holder immediately after it's been populated. PiperOrigin-RevId: 262362725 --- .../android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 2 +- .../google/android/exoplayer2/metadata/MetadataRenderer.java | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index 5e9d8d0897..b000ea1b6b 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -847,7 +847,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { pendingFormat = null; } inputBuffer.flip(); - inputBuffer.colorInfo = formatHolder.format.colorInfo; + inputBuffer.colorInfo = format.colorInfo; onQueueInputBuffer(inputBuffer); decoder.queueInputBuffer(inputBuffer); buffersInCodecCount++; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index a775481633..0fc0a85104 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -52,6 +52,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { private int pendingMetadataCount; private MetadataDecoder decoder; private boolean inputStreamEnded; + private long subsampleOffsetUs; /** * @param output The output. @@ -120,7 +121,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { // If we ever need to support a metadata format where this is not the case, we'll need to // pass the buffer to the decoder and discard the output. } else { - buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; + buffer.subsampleOffsetUs = subsampleOffsetUs; buffer.flip(); int index = (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT; Metadata metadata = decoder.decode(buffer); @@ -130,6 +131,8 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { pendingMetadataCount++; } } + } else if (result == C.RESULT_FORMAT_READ) { + subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; } } From 389eca6e077549fbe740e5975fe93daf573aaa2d Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 8 Aug 2019 16:59:19 +0100 Subject: [PATCH 1555/1556] Merge robolectric_testutils into testutils. We no longer need two modules as AndroidX-Test takes care of the system abstraction and we no longer have Robolectric Handler/Looper workarounds. PiperOrigin-RevId: 262363201 --- core_settings.gradle | 2 - extensions/cast/build.gradle | 3 +- extensions/cronet/build.gradle | 3 +- extensions/ffmpeg/build.gradle | 3 +- extensions/flac/build.gradle | 3 +- extensions/ima/build.gradle | 3 +- extensions/opus/build.gradle | 3 +- extensions/rtmp/build.gradle | 3 +- extensions/vp9/build.gradle | 3 +- library/core/build.gradle | 1 - library/dash/build.gradle | 3 +- library/hls/build.gradle | 3 +- library/smoothstreaming/build.gradle | 3 +- library/ui/build.gradle | 3 +- testutils/build.gradle | 4 +- .../exoplayer2/testutil/CacheAsserts.java | 0 .../DefaultRenderersFactoryAsserts.java | 0 .../exoplayer2/testutil/FakeMediaChunk.java | 0 .../testutil/FakeMediaChunkIterator.java | 0 .../testutil/FakeMediaClockRenderer.java | 0 .../exoplayer2/testutil/FakeShuffleOrder.java | 0 .../testutil/FakeTrackSelection.java | 0 .../testutil/FakeTrackSelector.java | 0 .../testutil/MediaPeriodAsserts.java | 0 .../testutil/MediaSourceTestRunner.java | 0 .../exoplayer2/testutil/OggTestData.java | 0 .../exoplayer2/testutil/StubExoPlayer.java | 0 .../testutil/TestDownloadManagerListener.java | 0 .../exoplayer2/testutil/TimelineAsserts.java | 0 testutils_robolectric/build.gradle | 46 ------------------- .../src/main/AndroidManifest.xml | 17 ------- 31 files changed, 27 insertions(+), 79 deletions(-) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/DefaultRenderersFactoryAsserts.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java (100%) delete mode 100644 testutils_robolectric/build.gradle delete mode 100644 testutils_robolectric/src/main/AndroidManifest.xml diff --git a/core_settings.gradle b/core_settings.gradle index 38889e1a21..3f6d58f777 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -24,7 +24,6 @@ include modulePrefix + 'library-hls' include modulePrefix + 'library-smoothstreaming' include modulePrefix + 'library-ui' include modulePrefix + 'testutils' -include modulePrefix + 'testutils-robolectric' include modulePrefix + 'extension-ffmpeg' include modulePrefix + 'extension-flac' include modulePrefix + 'extension-gvr' @@ -47,7 +46,6 @@ project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hl project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming') project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui') project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils') -project(modulePrefix + 'testutils-robolectric').projectDir = new File(rootDir, 'testutils_robolectric') project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg') project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac') project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 68a7494a3f..4af8f94c58 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -37,7 +37,8 @@ dependencies { implementation project(modulePrefix + 'library-ui') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index f7cc707fb4..9c49ba94e1 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -36,7 +36,8 @@ dependencies { implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'library') - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index 15952b1860..2b5a6010a9 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -40,7 +40,8 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index c67de27697..dfac2e1c26 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -43,7 +43,8 @@ dependencies { compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 0ef9f281c9..41d6aaf628 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -36,7 +36,8 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.1.0' implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0' - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index 28f7b05465..7b621a8df9 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -40,7 +40,8 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.1.0' - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion } diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index b74be659ee..74ef70fbf0 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -34,7 +34,8 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'net.butterflytv.utils:rtmp-client:3.0.1' implementation 'androidx.annotation:annotation:1.1.0' - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index 51b2677368..3b8271869b 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -40,7 +40,8 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.1.0' - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion diff --git a/library/core/build.gradle b/library/core/build.gradle index 93126d9830..fda2f079de 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -42,7 +42,6 @@ android { } test { java.srcDirs += '../../testutils/src/main/java/' - java.srcDirs += '../../testutils_robolectric/src/main/java/' } } diff --git a/library/dash/build.gradle b/library/dash/build.gradle index c34ed8c907..c64da2b86d 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -43,7 +43,8 @@ dependencies { compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion implementation 'androidx.annotation:annotation:1.1.0' - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 8301820e79..0f685c1130 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -43,7 +43,8 @@ dependencies { compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion implementation project(modulePrefix + 'library-core') - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index d85ecbb1a3..b16157f49b 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -43,7 +43,8 @@ dependencies { compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion implementation 'androidx.annotation:annotation:1.1.0' - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 5182dfccf5..5b3123e302 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -43,7 +43,8 @@ dependencies { implementation 'androidx.media:media:1.0.1' implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/testutils/build.gradle b/testutils/build.gradle index afd2a146af..b5e68187be 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -39,11 +39,13 @@ android { dependencies { api 'org.mockito:mockito-core:' + mockitoVersion + api 'androidx.test:core:' + androidXTestVersion api 'androidx.test.ext:junit:' + androidXTestVersion api 'com.google.truth:truth:' + truthVersion implementation 'androidx.annotation:annotation:1.1.0' implementation project(modulePrefix + 'library-core') implementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion annotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/DefaultRenderersFactoryAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DefaultRenderersFactoryAsserts.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/DefaultRenderersFactoryAsserts.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/DefaultRenderersFactoryAsserts.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java diff --git a/testutils_robolectric/build.gradle b/testutils_robolectric/build.gradle deleted file mode 100644 index a098178429..0000000000 --- a/testutils_robolectric/build.gradle +++ /dev/null @@ -1,46 +0,0 @@ -// 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. -apply from: '../constants.gradle' -apply plugin: 'com.android.library' - -android { - compileSdkVersion project.ext.compileSdkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - defaultConfig { - minSdkVersion project.ext.minSdkVersion - targetSdkVersion project.ext.targetSdkVersion - } - - lintOptions { - // Robolectric depends on BouncyCastle, which depends on javax.naming, - // which is not part of Android. - disable 'InvalidPackage' - } - - testOptions.unitTests.includeAndroidResources = true -} - -dependencies { - api 'androidx.test:core:' + androidXTestVersion - api 'org.robolectric:robolectric:' + robolectricVersion - api project(modulePrefix + 'testutils') - implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.1.0' - annotationProcessor 'com.google.auto.service:auto-service:' + autoServiceVersion -} diff --git a/testutils_robolectric/src/main/AndroidManifest.xml b/testutils_robolectric/src/main/AndroidManifest.xml deleted file mode 100644 index 057caad867..0000000000 --- a/testutils_robolectric/src/main/AndroidManifest.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - From 8967dd9c4c48980e433027b8dfb295b1cf99d7cd Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 9 Aug 2019 08:33:42 +0100 Subject: [PATCH 1556/1556] Upgrade IMA dependency version PiperOrigin-RevId: 262511088 --- extensions/ima/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 41d6aaf628..340e9832be 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -32,7 +32,7 @@ android { } dependencies { - api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2' + api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.3' implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.1.0' implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'