From c4fff54d2ca0ce69ce9c7b63ae5082fa16091c4f Mon Sep 17 00:00:00 2001 From: Andrew Orobator Date: Wed, 3 May 2017 10:49:36 -0400 Subject: [PATCH 001/220] Improved Documentation Added missing coma and fixed typo for EventListener#onTimelineChanged --- .../main/java/com/google/android/exoplayer2/ExoPlayer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 ab521e3733..82ec610be7 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 @@ -113,8 +113,8 @@ public interface ExoPlayer { * Called when the timeline and/or manifest 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. The will not be reported via a separate call to + * 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()}. * * @param timeline The latest timeline. Never null, but may be empty. From 5c723f4d3da3162cea11b9fc49aea004a3d2c87e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 27 Apr 2017 07:49:14 -0700 Subject: [PATCH 002/220] Prevent text tracks with no language being selected by default ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=154421706 --- .../exoplayer2/trackselection/DefaultTrackSelector.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 9db77fd7ad..941df66e4d 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 @@ -867,7 +867,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } protected static boolean formatHasLanguage(Format format, String language) { - return TextUtils.equals(language, Util.normalizeLanguageCode(format.language)); + return language != null + && TextUtils.equals(language, Util.normalizeLanguageCode(format.language)); } // Viewport size util methods. From 6d2fa12e163034fd8cc5321313760ae657cd8128 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 28 Apr 2017 02:11:03 -0700 Subject: [PATCH 003/220] Add getNextWindowIndex to Timeline (Preparation for Repeat Toggle Function - GitHub Issue #2577) In addition, Timeline now also got a getPreviousWindowIndex and a getNextPeriodIndex method with default implementations. Changed ExoPlayerImplInternal and PlaybackControlView to use these methods at all occurances of period and window index operations. Note: Does not include repeat mode yet and no timelines are actually using it so far. Please wait for the next CLs for this. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=154520664 --- .../google/android/exoplayer2/ExoPlayer.java | 14 +++ .../exoplayer2/ExoPlayerImplInternal.java | 30 ++++--- .../google/android/exoplayer2/Timeline.java | 86 +++++++++++++++++++ .../exoplayer2/ui/PlaybackControlView.java | 18 ++-- 4 files changed, 130 insertions(+), 18 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 ab521e3733..e168505d05 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.IntDef; import android.support.annotation.Nullable; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer; @@ -30,6 +31,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * An extensible media player exposing traditional high-level media player functionality, such as @@ -251,6 +254,17 @@ public interface ExoPlayer { */ int STATE_ENDED = 4; + /** + * Repeat modes for playback. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({REPEAT_MODE_OFF}) + @interface RepeatMode {} + /** + * Normal playback without repetition. + */ + int REPEAT_MODE_OFF = 0; + /** * Register a listener to receive events from the player. The listener's methods will be called on * the thread that was used to construct the player. 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 bf5b3f6482..2410e19f04 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 @@ -948,10 +948,7 @@ import java.io.IOException; } // The current period is in the new timeline. Update the holder and playbackInfo. - timeline.getPeriod(periodIndex, period); - boolean isLastPeriod = periodIndex == timeline.getPeriodCount() - 1 - && !timeline.getWindow(period.windowIndex, window).isDynamic; - periodHolder.setIndex(periodIndex, isLastPeriod); + periodHolder.setIndex(periodIndex, isLastPeriod(periodIndex)); boolean seenReadingPeriod = periodHolder == readingPeriodHolder; if (periodIndex != playbackInfo.periodIndex) { playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); @@ -962,10 +959,10 @@ import java.io.IOException; while (periodHolder.next != null) { MediaPeriodHolder previousPeriodHolder = periodHolder; periodHolder = periodHolder.next; - periodIndex++; + periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, + ExoPlayer.REPEAT_MODE_OFF); + boolean isLastPeriod = isLastPeriod(periodIndex); timeline.getPeriod(periodIndex, period, true); - isLastPeriod = periodIndex == timeline.getPeriodCount() - 1 - && !timeline.getWindow(period.windowIndex, window).isDynamic; if (periodHolder.uid.equals(period.uid)) { // The holder is consistent with the new timeline. Update its index and continue. periodHolder.setIndex(periodIndex, isLastPeriod); @@ -1023,13 +1020,22 @@ import java.io.IOException; private int resolveSubsequentPeriod(int oldPeriodIndex, Timeline oldTimeline, Timeline newTimeline) { int newPeriodIndex = C.INDEX_UNSET; - while (newPeriodIndex == C.INDEX_UNSET && oldPeriodIndex < oldTimeline.getPeriodCount() - 1) { + int maxIterations = oldTimeline.getPeriodCount(); + for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) { + oldPeriodIndex = oldTimeline.getNextPeriodIndex(oldPeriodIndex, period, window, + ExoPlayer.REPEAT_MODE_OFF); newPeriodIndex = newTimeline.getIndexOfPeriod( - oldTimeline.getPeriod(++oldPeriodIndex, period, true).uid); + oldTimeline.getPeriod(oldPeriodIndex, period, true).uid); } return newPeriodIndex; } + private boolean isLastPeriod(int periodIndex) { + int windowIndex = timeline.getPeriod(periodIndex, period).windowIndex; + return !timeline.getWindow(windowIndex, window).isDynamic + && timeline.isLastPeriod(periodIndex, period, window, ExoPlayer.REPEAT_MODE_OFF); + } + /** * Converts a {@link SeekPosition} into the corresponding (periodIndex, periodPositionUs) for the * internal timeline. @@ -1240,7 +1246,8 @@ import java.io.IOException; // We are already buffering the maximum number of periods ahead. return; } - newLoadingPeriodIndex = loadingPeriodHolder.index + 1; + newLoadingPeriodIndex = timeline.getNextPeriodIndex(loadingPeriodHolder.index, period, window, + ExoPlayer.REPEAT_MODE_OFF); } if (newLoadingPeriodIndex >= timeline.getPeriodCount()) { @@ -1283,9 +1290,8 @@ import java.io.IOException; ? newLoadingPeriodStartPositionUs + RENDERER_TIMESTAMP_OFFSET_US : (loadingPeriodHolder.getRendererOffset() + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()); + boolean isLastPeriod = isLastPeriod(newLoadingPeriodIndex); timeline.getPeriod(newLoadingPeriodIndex, period, true); - boolean isLastPeriod = newLoadingPeriodIndex == timeline.getPeriodCount() - 1 - && !timeline.getWindow(period.windowIndex, window).isDynamic; MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, period.uid, newLoadingPeriodIndex, isLastPeriod, newLoadingPeriodStartPositionUs); 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 eb3966ae4d..8dc30b0905 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 @@ -136,6 +136,54 @@ public abstract class Timeline { */ public abstract int getWindowCount(); + /** + * Returns the index of the window after the window at index {@code windowIndex} depending on the + * {@code repeatMode}. + * + * @param windowIndex Index of a window in the timeline. + * @param repeatMode A repeat mode. + * @return The index of the next window, or {@link C#INDEX_UNSET} if this is the last window. + */ + public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + return windowIndex == getWindowCount() - 1 ? C.INDEX_UNSET : windowIndex + 1; + } + + /** + * Returns the index of the window before the window at index {@code windowIndex} depending on the + * {@code repeatMode}. + * + * @param windowIndex Index of a window in the timeline. + * @param repeatMode A repeat mode. + * @return The index of the previous window, or {@link C#INDEX_UNSET} if this is the first window. + */ + public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + return windowIndex == 0 ? C.INDEX_UNSET : windowIndex - 1; + } + + /** + * Returns whether the given window is the last window of the timeline depending on the + * {@code repeatMode}. + * + * @param windowIndex A window index. + * @param repeatMode A repeat mode. + * @return Whether the window of the given index is the last window of the timeline. + */ + public final boolean isLastWindow(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + return getNextWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET; + } + + /** + * Returns whether the given window is the first window of the timeline depending on the + * {@code repeatMode}. + * + * @param windowIndex A window index. + * @param repeatMode A repeat mode. + * @return Whether the window of the given index is the first window of the timeline. + */ + public final boolean isFirstWindow(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + return getPreviousWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET; + } + /** * Populates a {@link Window} with data for the window at the specified index. Does not populate * {@link Window#id}. @@ -180,6 +228,44 @@ public abstract class Timeline { */ public abstract int getPeriodCount(); + /** + * Returns the index of the period after the period at index {@code periodIndex} depending on the + * {@code repeatMode}. + * + * @param periodIndex Index of a period in the timeline. + * @param period A {@link Period} to be used internally. Must not be null. + * @param window A {@link Window} to be used internally. Must not be null. + * @param repeatMode A repeat mode. + * @return The index of the next period, or {@link C#INDEX_UNSET} if this is the last period. + */ + public final int getNextPeriodIndex(int periodIndex, Period period, Window window, + @ExoPlayer.RepeatMode int repeatMode) { + int windowIndex = getPeriod(periodIndex, period).windowIndex; + if (getWindow(windowIndex, window).lastPeriodIndex == periodIndex) { + int nextWindowIndex = getNextWindowIndex(windowIndex, repeatMode); + if (nextWindowIndex == C.INDEX_UNSET) { + return C.INDEX_UNSET; + } + return getWindow(nextWindowIndex, window).firstPeriodIndex; + } + return periodIndex + 1; + } + + /** + * Returns whether the given period is the last period of the timeline depending on the + * {@code repeatMode}. + * + * @param periodIndex A period index. + * @param period A {@link Period} to be used internally. Must not be null. + * @param window A {@link Window} to be used internally. Must not be null. + * @param repeatMode A repeat mode. + * @return Whether the period of the given index is the last period of the timeline. + */ + public final boolean isLastPeriod(int periodIndex, Period period, Window window, + @ExoPlayer.RepeatMode int repeatMode) { + return getNextPeriodIndex(periodIndex, period, window, repeatMode) == C.INDEX_UNSET; + } + /** * Populates a {@link Period} with data for the period at the specified index. Does not populate * {@link Period#id} and {@link Period#uid}. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index ce2e81020f..baeada098a 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -529,8 +529,10 @@ public class PlaybackControlView extends FrameLayout { int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); isSeekable = window.isSeekable; - enablePrevious = windowIndex > 0 || isSeekable || !window.isDynamic; - enableNext = (windowIndex < timeline.getWindowCount() - 1) || window.isDynamic; + enablePrevious = !timeline.isFirstWindow(windowIndex, ExoPlayer.REPEAT_MODE_OFF) + || isSeekable || !window.isDynamic; + enableNext = !timeline.isLastWindow(windowIndex, ExoPlayer.REPEAT_MODE_OFF) + || window.isDynamic; if (timeline.getPeriod(player.getCurrentPeriodIndex(), period).isAd) { // Always hide player controls during ads. hide(); @@ -680,9 +682,12 @@ public class PlaybackControlView extends FrameLayout { } int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); - if (windowIndex > 0 && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS + int previousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, + ExoPlayer.REPEAT_MODE_OFF); + if (previousWindowIndex != C.INDEX_UNSET + && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS || (window.isDynamic && !window.isSeekable))) { - seekTo(windowIndex - 1, C.TIME_UNSET); + seekTo(previousWindowIndex, C.TIME_UNSET); } else { seekTo(0); } @@ -694,8 +699,9 @@ public class PlaybackControlView extends FrameLayout { return; } int windowIndex = player.getCurrentWindowIndex(); - if (windowIndex < timeline.getWindowCount() - 1) { - seekTo(windowIndex + 1, C.TIME_UNSET); + int nextWindowIndex = timeline.getNextWindowIndex(windowIndex, ExoPlayer.REPEAT_MODE_OFF); + if (nextWindowIndex != C.INDEX_UNSET) { + seekTo(nextWindowIndex, C.TIME_UNSET); } else if (timeline.getWindow(windowIndex, window, false).isDynamic) { seekTo(windowIndex, C.TIME_UNSET); } From 94ffbb2b6ac2f210b6fd59086cb42364476c7aad Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 2 May 2017 02:34:59 -0700 Subject: [PATCH 004/220] Support for Cronet from GMSCore (Blaze only) The blaze BUILD file for the Cronet extension now has three options: Using native bundled Cronet libs, using GMSCore, or using whichever is newer. The GMSCore version is preselected (as it is the smallest), but other variants may be used by uncommenting the respective lines. The API is the same for all cases and the CronetEngine.Builder automatically selects the newest option or falls back to default http. To avoid that apps using this extension need to add a dependency to Cronet themselves, I added a CronetEngineFactory to the Exoplayer extension. Gradle builds can't be supported (as far as I can see), as the GMSCore Cronet version is first party only. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=154812029 --- extensions/cronet/src/main/gcore_versions.bzl | 5 +++ .../ext/cronet/CronetDataSourceFactory.java | 17 +++---- .../ext/cronet/CronetEngineFactory.java | 45 +++++++++++++++++++ 3 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 extensions/cronet/src/main/gcore_versions.bzl create mode 100644 extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineFactory.java diff --git a/extensions/cronet/src/main/gcore_versions.bzl b/extensions/cronet/src/main/gcore_versions.bzl new file mode 100644 index 0000000000..7f9f9c3863 --- /dev/null +++ b/extensions/cronet/src/main/gcore_versions.bzl @@ -0,0 +1,5 @@ +"""GCore versions supporting Cronet.""" +GCORE_VERSIONS = [ + "v10", +] + 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 2ad6da6a54..1af76c11a7 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 @@ -22,7 +22,6 @@ import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Predicate; import java.util.concurrent.Executor; -import org.chromium.net.CronetEngine; /** * A {@link Factory} that produces {@link CronetDataSource}. @@ -34,13 +33,14 @@ public final class CronetDataSourceFactory extends BaseFactory { */ public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = CronetDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS; + /** * The default read timeout, in milliseconds. */ public static final int DEFAULT_READ_TIMEOUT_MILLIS = CronetDataSource.DEFAULT_READ_TIMEOUT_MILLIS; - private final CronetEngine cronetEngine; + private final CronetEngineFactory cronetEngineFactory; private final Executor executor; private final Predicate contentTypePredicate; private final TransferListener transferListener; @@ -48,18 +48,18 @@ public final class CronetDataSourceFactory extends BaseFactory { private final int readTimeoutMs; private final boolean resetTimeoutOnRedirects; - public CronetDataSourceFactory(CronetEngine cronetEngine, + public CronetDataSourceFactory(CronetEngineFactory cronetEngineFactory, Executor executor, Predicate contentTypePredicate, TransferListener transferListener) { - this(cronetEngine, executor, contentTypePredicate, transferListener, + this(cronetEngineFactory, executor, contentTypePredicate, transferListener, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false); } - public CronetDataSourceFactory(CronetEngine cronetEngine, + public CronetDataSourceFactory(CronetEngineFactory cronetEngineFactory, Executor executor, Predicate contentTypePredicate, TransferListener transferListener, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects) { - this.cronetEngine = cronetEngine; + this.cronetEngineFactory = cronetEngineFactory; this.executor = executor; this.contentTypePredicate = contentTypePredicate; this.transferListener = transferListener; @@ -71,8 +71,9 @@ public final class CronetDataSourceFactory extends BaseFactory { @Override protected CronetDataSource createDataSourceInternal(HttpDataSource.RequestProperties defaultRequestProperties) { - return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener, - connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties); + return new CronetDataSource(cronetEngineFactory.createCronetEngine(), executor, + contentTypePredicate, transferListener, connectTimeoutMs, readTimeoutMs, + resetTimeoutOnRedirects, defaultRequestProperties); } } diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineFactory.java new file mode 100644 index 0000000000..0bd74256e4 --- /dev/null +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineFactory.java @@ -0,0 +1,45 @@ +/* + * 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.ext.cronet; + +import android.content.Context; +import org.chromium.net.CronetEngine; + +/** + * A factory class which creates or reuses a {@link CronetEngine}. + */ +public final class CronetEngineFactory { + + private final Context context; + + private CronetEngine cronetEngine = null; + + /** + * Creates the factory for a {@link CronetEngine}. + * @param context The application context. + */ + public CronetEngineFactory(Context context) { + this.context = context; + } + + /* package */ CronetEngine createCronetEngine() { + if (cronetEngine == null) { + cronetEngine = new CronetEngine.Builder(context).build(); + } + return cronetEngine; + } + +} From ab30d715c7b61d63ee664a48a03ca7a7a67ffa64 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 2 May 2017 04:09:00 -0700 Subject: [PATCH 005/220] Infinite loops using new Timeline features Using the new getNextWindowIndex method of Timeline, LoopingMediaSource now uses a InfinitelyLoopingTimeline which does not unroll the windows to 157 million iterations but just starts from the beginning. If an explicit number of iterations is given, we still unroll. This change also allows multi-window timebars to show infinitely looping playlists correctly. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=154817554 --- .../exoplayer2/source/LoopingMediaSource.java | 79 +++++++++++++------ 1 file changed, 55 insertions(+), 24 deletions(-) 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 8b14c78234..0c872f199c 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 @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.source; -import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; @@ -29,13 +28,6 @@ import java.io.IOException; */ public final class LoopingMediaSource implements MediaSource { - /** - * The maximum number of periods that can be exposed by the source. The value of this constant is - * large enough to cause indefinite looping in practice (the total duration of the looping source - * will be approximately five years if the duration of each period is one second). - */ - public static final int MAX_EXPOSED_PERIODS = 157680000; - private static final String TAG = "LoopingMediaSource"; private final MediaSource childSource; @@ -56,9 +48,7 @@ public final class LoopingMediaSource implements MediaSource { * Loops the provided source a specified number of times. * * @param childSource The {@link MediaSource} to loop. - * @param loopCount The desired number of loops. Must be strictly positive. The actual number of - * loops will be capped at the maximum that can achieved without causing the number of - * periods exposed by the source to exceed {@link #MAX_EXPOSED_PERIODS}. + * @param loopCount The desired number of loops. Must be strictly positive. */ public LoopingMediaSource(MediaSource childSource, int loopCount) { Assertions.checkArgument(loopCount > 0); @@ -72,7 +62,9 @@ public final class LoopingMediaSource implements MediaSource { @Override public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { childPeriodCount = timeline.getPeriodCount(); - listener.onSourceInfoRefreshed(new LoopingTimeline(timeline, loopCount), manifest); + Timeline loopingTimeline = loopCount != Integer.MAX_VALUE + ? new LoopingTimeline(timeline, loopCount) : new InfinitelyLoopingTimeline(timeline); + listener.onSourceInfoRefreshed(loopingTimeline, manifest); } }); } @@ -84,7 +76,9 @@ public final class LoopingMediaSource implements MediaSource { @Override public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - return childSource.createPeriod(index % childPeriodCount, allocator, positionUs); + return loopCount != Integer.MAX_VALUE + ? childSource.createPeriod(index % childPeriodCount, allocator, positionUs) + : childSource.createPeriod(index, allocator, positionUs); } @Override @@ -108,17 +102,9 @@ public final class LoopingMediaSource implements MediaSource { this.childTimeline = childTimeline; childPeriodCount = childTimeline.getPeriodCount(); childWindowCount = childTimeline.getWindowCount(); - // This is the maximum number of loops that can be performed without exceeding - // MAX_EXPOSED_PERIODS periods. - int maxLoopCount = MAX_EXPOSED_PERIODS / childPeriodCount; - if (loopCount > maxLoopCount) { - if (loopCount != Integer.MAX_VALUE) { - Log.w(TAG, "Capped loops to avoid overflow: " + loopCount + " -> " + maxLoopCount); - } - this.loopCount = maxLoopCount; - } else { - this.loopCount = loopCount; - } + this.loopCount = loopCount; + Assertions.checkState(loopCount <= Integer.MAX_VALUE / childPeriodCount, + "LoopingMediaSource contains too many periods"); } @Override @@ -169,4 +155,49 @@ public final class LoopingMediaSource implements MediaSource { } + private static final class InfinitelyLoopingTimeline extends Timeline { + + private final Timeline childTimeline; + + public InfinitelyLoopingTimeline(Timeline childTimeline) { + this.childTimeline = childTimeline; + } + + @Override + public int getWindowCount() { + return childTimeline.getWindowCount(); + } + + @Override + public int getNextWindowIndex(int currentWindowIndex, @ExoPlayer.RepeatMode int repeatMode) { + return currentWindowIndex < getWindowCount() - 1 ? currentWindowIndex + 1 : 0; + } + + @Override + public int getPreviousWindowIndex(int currentWindowIndex, + @ExoPlayer.RepeatMode int repeatMode) { + return currentWindowIndex > 0 ? currentWindowIndex - 1 : getWindowCount() - 1; + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + return childTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); + } + + @Override + public int getPeriodCount() { + return childTimeline.getPeriodCount(); + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + return childTimeline.getPeriod(periodIndex, period, setIds); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return childTimeline.getIndexOfPeriod(uid); + } + } } From d33a6b49f0ee3a36b9236bc76bc765fe0b7fd670 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 2 May 2017 05:15:56 -0700 Subject: [PATCH 006/220] User-defined fallback if Cronet is not available When using the CronetEngine.Builder class, it automatically selects the Cronet version preferring higher version codes and falling back to a Java Http implementation if no native or GMSCore version is available. This version selection has now been moved into the CronetEngineFactory class to always prefer GMSCore over natively bundled versions. We also ignore the Cronet internal Java implementation. Instead, users of CronetDataSourceFactory can provide their own fallback factory. If none is provided, we use DefaultHttpDataSourceFactory. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=154821040 --- .../ext/cronet/CronetDataSourceFactory.java | 110 ++++++++++++++- .../ext/cronet/CronetEngineFactory.java | 133 +++++++++++++++++- 2 files changed, 235 insertions(+), 8 deletions(-) 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 1af76c11a7..2e4c27a920 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 @@ -16,12 +16,15 @@ package com.google.android.exoplayer2.ext.cronet; import com.google.android.exoplayer2.upstream.DataSource; +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; /** * A {@link Factory} that produces {@link CronetDataSource}. @@ -47,18 +50,112 @@ public final class CronetDataSourceFactory extends BaseFactory { private final int connectTimeoutMs; private final int readTimeoutMs; private final boolean resetTimeoutOnRedirects; + private final HttpDataSource.Factory fallbackFactory; + /** + * Constructs a CronetDataSourceFactory. + *

+ * If the {@link CronetEngineFactory} fails to provide a suitable {@link CronetEngine}, the + * provided fallback {@link HttpDataSource.Factory} will be used instead. + * + * Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link + * CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables + * cross-protocol redirects. + * + * @param cronetEngineFactory A {@link CronetEngineFactory}. + * @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. + */ public CronetDataSourceFactory(CronetEngineFactory cronetEngineFactory, Executor executor, Predicate contentTypePredicate, - TransferListener transferListener) { + TransferListener transferListener, + HttpDataSource.Factory fallbackFactory) { this(cronetEngineFactory, executor, contentTypePredicate, transferListener, - DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false); + DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory); } + /** + * Constructs a CronetDataSourceFactory. + *

+ * If the {@link CronetEngineFactory} fails to provide a suitable {@link CronetEngine}, a + * {@link DefaultHttpDataSourceFactory} will be used instead. + * + * Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link + * CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables + * cross-protocol redirects. + * + * @param cronetEngineFactory A {@link CronetEngineFactory}. + * @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(CronetEngineFactory cronetEngineFactory, + Executor executor, Predicate contentTypePredicate, + TransferListener transferListener, String userAgent) { + this(cronetEngineFactory, 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)); + } + + /** + * Constructs a CronetDataSourceFactory. + *

+ * If the {@link CronetEngineFactory} fails to provide a suitable {@link CronetEngine}, a + * {@link DefaultHttpDataSourceFactory} will be used instead. + * + * @param cronetEngineFactory A {@link CronetEngineFactory}. + * @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. + * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. + * @param userAgent A user agent used to create a fallback HttpDataSource if needed. + */ public CronetDataSourceFactory(CronetEngineFactory cronetEngineFactory, Executor executor, Predicate contentTypePredicate, TransferListener transferListener, int connectTimeoutMs, - int readTimeoutMs, boolean resetTimeoutOnRedirects) { + int readTimeoutMs, boolean resetTimeoutOnRedirects, String userAgent) { + this(cronetEngineFactory, executor, contentTypePredicate, transferListener, + DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects, + new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs, + readTimeoutMs, resetTimeoutOnRedirects)); + } + + /** + * Constructs a CronetDataSourceFactory. + *

+ * If the {@link CronetEngineFactory} fails to provide a suitable {@link CronetEngine}, the + * provided fallback {@link HttpDataSource.Factory} will be used instead. + * + * @param cronetEngineFactory A {@link CronetEngineFactory}. + * @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. + * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. + * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case + * no suitable CronetEngine can be build. + */ + public CronetDataSourceFactory(CronetEngineFactory cronetEngineFactory, + Executor executor, Predicate contentTypePredicate, + TransferListener transferListener, int connectTimeoutMs, + int readTimeoutMs, boolean resetTimeoutOnRedirects, + HttpDataSource.Factory fallbackFactory) { this.cronetEngineFactory = cronetEngineFactory; this.executor = executor; this.contentTypePredicate = contentTypePredicate; @@ -66,11 +163,16 @@ public final class CronetDataSourceFactory extends BaseFactory { this.connectTimeoutMs = connectTimeoutMs; this.readTimeoutMs = readTimeoutMs; this.resetTimeoutOnRedirects = resetTimeoutOnRedirects; + this.fallbackFactory = fallbackFactory; } @Override - protected CronetDataSource createDataSourceInternal(HttpDataSource.RequestProperties + protected HttpDataSource createDataSourceInternal(HttpDataSource.RequestProperties defaultRequestProperties) { + CronetEngine cronetEngine = cronetEngineFactory.createCronetEngine(); + if (cronetEngine == null) { + return fallbackFactory.createDataSource(); + } return new CronetDataSource(cronetEngineFactory.createCronetEngine(), executor, contentTypePredicate, transferListener, connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties); diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineFactory.java index 0bd74256e4..7211ea64f4 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineFactory.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineFactory.java @@ -16,30 +16,155 @@ package com.google.android.exoplayer2.ext.cronet; import android.content.Context; +import android.util.Log; +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; import org.chromium.net.CronetEngine; +import org.chromium.net.CronetProvider; /** * A factory class which creates or reuses a {@link CronetEngine}. */ public final class CronetEngineFactory { + private static final String TAG = "CronetEngineFactory"; + private final Context context; + private final boolean preferGMSCoreCronet; private CronetEngine cronetEngine = null; /** - * Creates the factory for a {@link CronetEngine}. - * @param context The application context. + * Creates the factory for a {@link CronetEngine}. Sets factory to prefer natively bundled Cronet + * over GMSCore Cronet if both are available. + * + * @param context A context. */ public CronetEngineFactory(Context context) { - this.context = context; + this(context, false); } + /** + * Creates the factory for a {@link CronetEngine} and specifies whether Cronet from GMSCore should + * be preferred over natively bundled Cronet if both are available. + * + * @param context A context. + */ + public CronetEngineFactory(Context context, boolean preferGMSCoreCronet) { + this.context = context.getApplicationContext(); + this.preferGMSCoreCronet = preferGMSCoreCronet; + } + + /** + * Create or reuse a {@link CronetEngine}. If no CronetEngine is available, the method returns + * null. + * + * @return The CronetEngine, or null if no CronetEngine is available. + */ /* package */ CronetEngine createCronetEngine() { if (cronetEngine == null) { - cronetEngine = new CronetEngine.Builder(context).build(); + List cronetProviders = CronetProvider.getAllProviders(context); + // Remove disabled and fallback Cronet providers from list + for (int i = cronetProviders.size() - 1; i >= 0; i--) { + if (!cronetProviders.get(i).isEnabled() + || CronetProvider.PROVIDER_NAME_FALLBACK.equals(cronetProviders.get(i).getName())) { + cronetProviders.remove(i); + } + } + // Sort remaining providers by type and version. + Collections.sort(cronetProviders, new CronetProviderComparator(preferGMSCoreCronet)); + for (int i = 0; i < cronetProviders.size(); i++) { + String providerName = cronetProviders.get(i).getName(); + try { + cronetEngine = cronetProviders.get(i).createBuilder().build(); + Log.d(TAG, "CronetEngine built using " + providerName); + } catch (UnsatisfiedLinkError e) { + Log.w(TAG, "Failed to link Cronet binaries. Please check if native Cronet binaries are " + + "bundled into your app."); + } + } + } + if (cronetEngine == null) { + Log.w(TAG, "Cronet not available. Using fallback provider."); } return cronetEngine; } + private static class CronetProviderComparator implements Comparator { + + private final String gmsCoreCronetName; + private final boolean preferGMSCoreCronet; + + public CronetProviderComparator(boolean preferGMSCoreCronet) { + // GMSCore CronetProvider classes are only available in some configurations. + // Thus, we use reflection to copy static name. + String gmsCoreVersionString = null; + try { + Class cronetProviderInstallerClass = + Class.forName("com.google.android.gms.net.CronetProviderInstaller"); + Field providerNameField = cronetProviderInstallerClass.getDeclaredField("PROVIDER_NAME"); + gmsCoreVersionString = (String) providerNameField.get(null); + } catch (ClassNotFoundException e) { + // GMSCore CronetProvider not available. + } catch (NoSuchFieldException e) { + // GMSCore CronetProvider not available. + } catch (IllegalAccessException e) { + // GMSCore CronetProvider not available. + } + gmsCoreCronetName = gmsCoreVersionString; + this.preferGMSCoreCronet = preferGMSCoreCronet; + } + + @Override + public int compare(CronetProvider providerLeft, CronetProvider providerRight) { + int typePreferenceLeft = evaluateCronetProviderType(providerLeft.getName()); + int typePreferenceRight = evaluateCronetProviderType(providerRight.getName()); + if (typePreferenceLeft != typePreferenceRight) { + return typePreferenceLeft - typePreferenceRight; + } + return -compareVersionStrings(providerLeft.getVersion(), providerRight.getVersion()); + } + + /** + * Convert Cronet provider name into a sortable preference value. + * Smaller values are preferred. + */ + private int evaluateCronetProviderType(String providerName) { + if (CronetProvider.PROVIDER_NAME_APP_PACKAGED.equals(providerName)) { + return 1; + } + if (gmsCoreCronetName != null && gmsCoreCronetName.equals(providerName)) { + return preferGMSCoreCronet ? 0 : 2; + } + // Unknown provider type. + return -1; + } + + /** + * Compares version strings of format "12.123.35.23". + */ + private static int compareVersionStrings(String versionLeft, String versionRight) { + if (versionLeft == null || versionRight == null) { + return 0; + } + String[] versionStringsLeft = versionLeft.split("\\."); + String[] versionStringsRight = versionRight.split("\\."); + int minLength = Math.min(versionStringsLeft.length, versionStringsRight.length); + for (int i = 0; i < minLength; i++) { + if (!versionStringsLeft[i].equals(versionStringsRight[i])) { + try { + int versionIntLeft = Integer.parseInt(versionStringsLeft[i]); + int versionIntRight = Integer.parseInt(versionStringsRight[i]); + return versionIntLeft - versionIntRight; + } catch (NumberFormatException e) { + return 0; + } + } + } + return 0; + } + } + } From c3158d3e682041f21cf0474dfdba6545f29be1ea Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 2 May 2017 06:38:57 -0700 Subject: [PATCH 007/220] Improve DefaultTimeBar color customization Add attributes for the scrubber handle color and unplayed color. If attributes are missing, derive defaults from the played color. Issue: #2740 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=154825736 --- .../android/exoplayer2/ui/DefaultTimeBar.java | 50 +++++++++++++------ library/ui/src/main/res/values/attrs.xml | 2 + 2 files changed, 36 insertions(+), 16 deletions(-) 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 d40da451a2..12f31f5da1 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 @@ -61,22 +61,21 @@ public class DefaultTimeBar extends View implements TimeBar { private static final int DEFAULT_INCREMENT_COUNT = 20; private static final int DEFAULT_BAR_HEIGHT = 4; private static final int DEFAULT_TOUCH_TARGET_HEIGHT = 26; - private static final int DEFAULT_PLAYED_COLOR = 0x33FFFFFF; - private static final int DEFAULT_BUFFERED_COLOR = 0xCCFFFFFF; + private static final int DEFAULT_PLAYED_COLOR = 0xFFFFFFFF; private static final int DEFAULT_AD_MARKER_COLOR = 0xB2FFFF00; private static final int DEFAULT_AD_MARKER_WIDTH = 4; private static final int DEFAULT_SCRUBBER_ENABLED_SIZE = 12; private static final int DEFAULT_SCRUBBER_DISABLED_SIZE = 0; private static final int DEFAULT_SCRUBBER_DRAGGED_SIZE = 16; - private static final int OPAQUE_COLOR = 0xFF000000; private final Rect seekBounds; private final Rect progressBar; private final Rect bufferedBar; private final Rect scrubberBar; - private final Paint progressPaint; - private final Paint bufferedPaint; + private final Paint playedPaint; private final Paint scrubberPaint; + private final Paint bufferedPaint; + private final Paint unplayedPaint; private final Paint adMarkerPaint; private final int barHeight; private final int touchTargetHeight; @@ -115,9 +114,10 @@ public class DefaultTimeBar extends View implements TimeBar { progressBar = new Rect(); bufferedBar = new Rect(); scrubberBar = new Rect(); - progressPaint = new Paint(); - bufferedPaint = new Paint(); + playedPaint = new Paint(); scrubberPaint = new Paint(); + bufferedPaint = new Paint(); + unplayedPaint = new Paint(); adMarkerPaint = new Paint(); // Calculate the dimensions and paints for drawn elements. @@ -147,13 +147,18 @@ public class DefaultTimeBar extends View implements TimeBar { scrubberDraggedSize = a.getDimensionPixelSize( R.styleable.DefaultTimeBar_scrubber_dragged_size, defaultScrubberDraggedSize); int playedColor = a.getInt(R.styleable.DefaultTimeBar_played_color, DEFAULT_PLAYED_COLOR); + int scrubberColor = a.getInt(R.styleable.DefaultTimeBar_scrubber_color, + getDefaultScrubberColor(playedColor)); int bufferedColor = a.getInt(R.styleable.DefaultTimeBar_buffered_color, - DEFAULT_BUFFERED_COLOR); + getDefaultBufferedColor(playedColor)); + int unplayedColor = a.getInt(R.styleable.DefaultTimeBar_unplayed_color, + getDefaultUnplayedColor(playedColor)); int adMarkerColor = a.getInt(R.styleable.DefaultTimeBar_ad_marker_color, DEFAULT_AD_MARKER_COLOR); - progressPaint.setColor(playedColor); - scrubberPaint.setColor(OPAQUE_COLOR | playedColor); + playedPaint.setColor(playedColor); + scrubberPaint.setColor(scrubberColor); bufferedPaint.setColor(bufferedColor); + unplayedPaint.setColor(unplayedColor); adMarkerPaint.setColor(adMarkerColor); } finally { a.recycle(); @@ -165,9 +170,10 @@ public class DefaultTimeBar extends View implements TimeBar { scrubberEnabledSize = defaultScrubberEnabledSize; scrubberDisabledSize = defaultScrubberDisabledSize; scrubberDraggedSize = defaultScrubberDraggedSize; - scrubberPaint.setColor(OPAQUE_COLOR | DEFAULT_PLAYED_COLOR); - progressPaint.setColor(DEFAULT_PLAYED_COLOR); - bufferedPaint.setColor(DEFAULT_BUFFERED_COLOR); + playedPaint.setColor(DEFAULT_PLAYED_COLOR); + scrubberPaint.setColor(getDefaultScrubberColor(DEFAULT_PLAYED_COLOR)); + bufferedPaint.setColor(getDefaultBufferedColor(DEFAULT_PLAYED_COLOR)); + unplayedPaint.setColor(getDefaultUnplayedColor(DEFAULT_PLAYED_COLOR)); adMarkerPaint.setColor(DEFAULT_AD_MARKER_COLOR); } formatBuilder = new StringBuilder(); @@ -502,21 +508,21 @@ public class DefaultTimeBar extends View implements TimeBar { int barTop = progressBar.centerY() - progressBarHeight / 2; int barBottom = barTop + progressBarHeight; if (duration <= 0) { - canvas.drawRect(progressBar.left, barTop, progressBar.right, barBottom, progressPaint); + canvas.drawRect(progressBar.left, barTop, progressBar.right, barBottom, unplayedPaint); return; } int bufferedLeft = bufferedBar.left; int bufferedRight = bufferedBar.right; int progressLeft = Math.max(Math.max(progressBar.left, bufferedRight), scrubberBar.right); if (progressLeft < progressBar.right) { - canvas.drawRect(progressLeft, barTop, progressBar.right, barBottom, progressPaint); + canvas.drawRect(progressLeft, barTop, progressBar.right, barBottom, unplayedPaint); } bufferedLeft = Math.max(bufferedLeft, scrubberBar.right); if (bufferedRight > bufferedLeft) { canvas.drawRect(bufferedLeft, barTop, bufferedRight, barBottom, bufferedPaint); } if (scrubberBar.width() > 0) { - canvas.drawRect(scrubberBar.left, barTop, scrubberBar.right, barBottom, scrubberPaint); + canvas.drawRect(scrubberBar.left, barTop, scrubberBar.right, barBottom, playedPaint); } int adMarkerOffset = adMarkerWidth / 2; for (int i = 0; i < adBreakCount; i++) { @@ -577,4 +583,16 @@ public class DefaultTimeBar extends View implements TimeBar { return (int) (dps * displayMetrics.density + 0.5f); } + private static int getDefaultScrubberColor(int playedColor) { + return 0xFF000000 | playedColor; + } + + private static int getDefaultUnplayedColor(int playedColor) { + return 0x33000000 | (playedColor & 0x00FFFFFF); + } + + private static int getDefaultBufferedColor(int playedColor) { + return 0xCC000000 | (playedColor & 0x00FFFFFF); + } + } diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 521e535ce3..d8340c21cd 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -68,7 +68,9 @@ + + From 6d01460b588afdc71294f2824477312e271ef93b Mon Sep 17 00:00:00 2001 From: falhassen Date: Tue, 2 May 2017 10:09:55 -0700 Subject: [PATCH 008/220] Use Looper.getMainLooper() in Handler constructors in ExoPlayer when needed. Looper.myLooper(), the default looper, may be null in background threads. This adds a fallback to use the main app looper. This will allow ExoPlayer instances to be built in background threads in Photos. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=154845446 --- .../google/android/exoplayer2/ExoPlayer.java | 8 ++++-- .../android/exoplayer2/ExoPlayerFactory.java | 28 ++++++------------- .../android/exoplayer2/ExoPlayerImpl.java | 3 +- .../android/exoplayer2/SimpleExoPlayer.java | 7 +++-- 4 files changed, 22 insertions(+), 24 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 e168505d05..be04924753 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.os.Looper; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; @@ -91,7 +92,9 @@ import java.lang.annotation.RetentionPolicy; * thread. The application's main thread is ideal. Accessing an instance from multiple threads is * discouraged, however if an application does wish to do this then it may do so provided that it * ensures accesses are synchronized. - *

  • Registered listeners are called on the thread that created the ExoPlayer instance.
  • + *
  • Registered listeners are called on the thread that created the ExoPlayer instance, unless + * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that case, + * registered listeners will be called on the application's main thread.
  • *
  • An internal playback thread is responsible for playback. Injected player components such as * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this * thread.
  • @@ -267,7 +270,8 @@ public interface ExoPlayer { /** * Register a listener to receive events from the player. The listener's methods will be called on - * the thread that was used to construct the player. + * the thread that was used to construct the player. However, if the thread used to construct the + * player does not have a {@link Looper}, then the listener will be called on the main thread. * * @param listener The listener to register. */ 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 7aecd20d4e..97a310c3da 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 @@ -16,7 +16,6 @@ package com.google.android.exoplayer2; import android.content.Context; -import android.os.Looper; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.trackselection.TrackSelector; @@ -29,8 +28,7 @@ public final class ExoPlayerFactory { private ExoPlayerFactory() {} /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. + * Creates a {@link SimpleExoPlayer} instance. * * @param context A {@link Context}. * @param trackSelector The {@link TrackSelector} that will be used by the instance. @@ -45,8 +43,7 @@ public final class ExoPlayerFactory { } /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. Available extension renderers are not used. + * Creates a {@link SimpleExoPlayer} instance. Available extension renderers are not used. * * @param context A {@link Context}. * @param trackSelector The {@link TrackSelector} that will be used by the instance. @@ -63,8 +60,7 @@ public final class ExoPlayerFactory { } /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. + * Creates a {@link SimpleExoPlayer} instance. * * @param context A {@link Context}. * @param trackSelector The {@link TrackSelector} that will be used by the instance. @@ -86,8 +82,7 @@ public final class ExoPlayerFactory { } /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. + * Creates a {@link SimpleExoPlayer} instance. * * @param context A {@link Context}. * @param trackSelector The {@link TrackSelector} that will be used by the instance. @@ -112,8 +107,7 @@ public final class ExoPlayerFactory { } /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. + * Creates a {@link SimpleExoPlayer} instance. * * @param context A {@link Context}. * @param trackSelector The {@link TrackSelector} that will be used by the instance. @@ -123,8 +117,7 @@ public final class ExoPlayerFactory { } /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. + * Creates a {@link SimpleExoPlayer} instance. * * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. @@ -135,8 +128,7 @@ public final class ExoPlayerFactory { } /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. + * Creates a {@link SimpleExoPlayer} instance. * * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. @@ -148,8 +140,7 @@ public final class ExoPlayerFactory { } /** - * Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. + * Creates an {@link ExoPlayer} instance. * * @param renderers The {@link Renderer}s that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. @@ -159,8 +150,7 @@ public final class ExoPlayerFactory { } /** - * Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. + * Creates an {@link ExoPlayer} instance. * * @param renderers The {@link Renderer}s that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. 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 4131b97954..cb0958a3b1 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 @@ -92,7 +92,8 @@ import java.util.concurrent.CopyOnWriteArraySet; trackGroups = TrackGroupArray.EMPTY; trackSelections = emptyTrackSelections; playbackParameters = PlaybackParameters.DEFAULT; - eventHandler = new Handler() { + Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); + eventHandler = new Handler(eventLooper) { @Override public void handleMessage(Message msg) { ExoPlayerImpl.this.handleEvent(msg); 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 28ba8cf9d7..6094513913 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 @@ -20,6 +20,7 @@ import android.graphics.SurfaceTexture; import android.media.MediaCodec; import android.media.PlaybackParams; import android.os.Handler; +import android.os.Looper; import android.support.annotation.Nullable; import android.util.Log; import android.view.Surface; @@ -111,8 +112,10 @@ public class SimpleExoPlayer implements ExoPlayer { protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl) { componentListener = new ComponentListener(); - renderers = renderersFactory.createRenderers(new Handler(), componentListener, - componentListener, componentListener, componentListener); + Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); + Handler eventHandler = new Handler(eventLooper); + renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, + componentListener, componentListener); // Obtain counts of video and audio renderers. int videoRendererCount = 0; From 7773831d88b910d323fa675dd3fb9aad98e3abfe Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 3 May 2017 01:32:22 -0700 Subject: [PATCH 009/220] Add DummySurface for use with MediaCodec A DummySurface is useful with MediaCodec on API levels 23+. Rather than having to release a MediaCodec instance when the app no longer has a real surface to output to, it's possible to retain the MediaCodec, using MediaCodec.setOutputSurface to target a DummySurface instance instead. When the app has a real surface to output to again, it can call swap this surface back in instantaneously. Without DummySurface a new MediaCodec has to be instantiated at this point, and decoding can only start from a key-frame in the media. A future change may hook this up internally in MediaCodecRenderer for supported use cases, although this looks a little awkward. If this approach isn't viable, we can require applications wanting this to set a DummySurface themselves. This isn't easy to do with the way SimpleExoPlayerView.setPlayer works at the moment, however, so some changes will be needed either way. Issue: #677 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=154931778 --- .../exoplayer2/video/DummySurface.java | 306 ++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java 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 new file mode 100644 index 0000000000..5298c82f61 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -0,0 +1,306 @@ +/* + * 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.video; + +import static android.opengl.EGL14.EGL_ALPHA_SIZE; +import static android.opengl.EGL14.EGL_BLUE_SIZE; +import static android.opengl.EGL14.EGL_CONFIG_CAVEAT; +import static android.opengl.EGL14.EGL_CONTEXT_CLIENT_VERSION; +import static android.opengl.EGL14.EGL_DEFAULT_DISPLAY; +import static android.opengl.EGL14.EGL_DEPTH_SIZE; +import static android.opengl.EGL14.EGL_GREEN_SIZE; +import static android.opengl.EGL14.EGL_HEIGHT; +import static android.opengl.EGL14.EGL_NONE; +import static android.opengl.EGL14.EGL_OPENGL_ES2_BIT; +import static android.opengl.EGL14.EGL_RED_SIZE; +import static android.opengl.EGL14.EGL_RENDERABLE_TYPE; +import static android.opengl.EGL14.EGL_SURFACE_TYPE; +import static android.opengl.EGL14.EGL_TRUE; +import static android.opengl.EGL14.EGL_WIDTH; +import static android.opengl.EGL14.EGL_WINDOW_BIT; +import static android.opengl.EGL14.eglChooseConfig; +import static android.opengl.EGL14.eglCreateContext; +import static android.opengl.EGL14.eglCreatePbufferSurface; +import static android.opengl.EGL14.eglGetDisplay; +import static android.opengl.EGL14.eglInitialize; +import static android.opengl.EGL14.eglMakeCurrent; +import static android.opengl.GLES20.glDeleteTextures; +import static android.opengl.GLES20.glGenTextures; + +import android.annotation.TargetApi; +import android.graphics.SurfaceTexture; +import android.graphics.SurfaceTexture.OnFrameAvailableListener; +import android.opengl.EGL14; +import android.opengl.EGLConfig; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLSurface; +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.HandlerThread; +import android.os.Message; +import android.util.Log; +import android.view.Surface; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import javax.microedition.khronos.egl.EGL10; + +/** + * A dummy {@link Surface}. + */ +@TargetApi(17) +public final class DummySurface extends Surface { + + private static final String TAG = "DummySurface"; + + private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; + + /** + * Whether the device supports secure dummy surfaces. + */ + public static final boolean SECURE_SUPPORTED; + static { + if (Util.SDK_INT >= 17) { + EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + String extensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); + SECURE_SUPPORTED = extensions.contains("EGL_EXT_protected_content"); + } else { + SECURE_SUPPORTED = false; + } + } + + /** + * Whether the surface is secure. + */ + public final boolean secure; + + private final DummySurfaceThread thread; + private boolean threadReleased; + + /** + * Returns a newly created dummy surface. The surface must be released by calling {@link #release} + * when it's no longer required. + *

    + * Must only be called if {@link Util#SDK_INT} is 17 or higher. + * + * @param secure Whether a secure surface is required. Must only be requested if + * {@link #SECURE_SUPPORTED} is {@code true}. + */ + public static DummySurface newInstanceV17(boolean secure) { + assertApiLevel17OrHigher(); + Assertions.checkState(!secure || SECURE_SUPPORTED); + DummySurfaceThread thread = new DummySurfaceThread(); + return thread.init(secure); + } + + private DummySurface(DummySurfaceThread thread, SurfaceTexture surfaceTexture, boolean secure) { + super(surfaceTexture); + this.thread = thread; + this.secure = secure; + } + + @Override + public void release() { + super.release(); + // The Surface may be released multiple times (explicitly and by Surface.finalize()). The + // implementation of super.release() has its own deduplication logic. Below we need to + // deduplicate ourselves. Synchronization is required as we don't control the thread on which + // Surface.finalize() is called. + synchronized (thread) { + if (!threadReleased) { + thread.release(); + threadReleased = true; + } + } + } + + private static void assertApiLevel17OrHigher() { + if (Util.SDK_INT < 17) { + throw new UnsupportedOperationException("Unsupported prior to API level 17"); + } + } + + private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, + Callback { + + private static final int MSG_INIT = 1; + private static final int MSG_UPDATE_TEXTURE = 2; + private static final int MSG_RELEASE = 3; + + private final int[] textureIdHolder; + private Handler handler; + private SurfaceTexture surfaceTexture; + + private Error initError; + private RuntimeException initException; + private DummySurface surface; + + public DummySurfaceThread() { + super("dummySurface"); + textureIdHolder = new int[1]; + } + + public DummySurface init(boolean secure) { + start(); + handler = new Handler(getLooper(), this); + boolean wasInterrupted = false; + synchronized (this) { + handler.obtainMessage(MSG_INIT, secure ? 1 : 0, 0).sendToTarget(); + while (surface == null && initException == null && initError == null) { + try { + wait(); + } catch (InterruptedException e) { + wasInterrupted = true; + } + } + } + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } + if (initException != null) { + throw initException; + } else if (initError != null) { + throw initError; + } else { + return surface; + } + } + + public void release() { + handler.sendEmptyMessage(MSG_RELEASE); + } + + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + handler.sendEmptyMessage(MSG_UPDATE_TEXTURE); + } + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_INIT: + try { + initInternal(msg.arg1 != 0); + } catch (RuntimeException e) { + Log.e(TAG, "Failed to initialize dummy surface", e); + initException = e; + } catch (Error e) { + Log.e(TAG, "Failed to initialize dummy surface", e); + initError = e; + } finally { + synchronized (this) { + notify(); + } + } + return true; + case MSG_UPDATE_TEXTURE: + surfaceTexture.updateTexImage(); + return true; + case MSG_RELEASE: + try { + releaseInternal(); + } catch (Throwable e) { + Log.e(TAG, "Failed to release dummy surface", e); + } finally { + quit(); + } + return true; + default: + return true; + } + } + + private void initInternal(boolean secure) { + EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + Assertions.checkState(display != null, "eglGetDisplay failed"); + + int[] version = new int[2]; + boolean eglInitialized = eglInitialize(display, version, 0, version, 1); + Assertions.checkState(eglInitialized, "eglInitialize failed"); + + int[] eglAttributes = new int[] { + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_DEPTH_SIZE, 0, + EGL_CONFIG_CAVEAT, EGL_NONE, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_NONE + }; + EGLConfig[] configs = new EGLConfig[1]; + int[] numConfigs = new int[1]; + boolean eglChooseConfigSuccess = eglChooseConfig(display, eglAttributes, 0, configs, 0, 1, + numConfigs, 0); + Assertions.checkState(eglChooseConfigSuccess && numConfigs[0] > 0 && configs[0] != null, + "eglChooseConfig failed"); + + EGLConfig config = configs[0]; + int[] glAttributes; + if (secure) { + glAttributes = new int[] { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_PROTECTED_CONTENT_EXT, + EGL_TRUE, EGL_NONE}; + } else { + glAttributes = new int[] { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE}; + } + EGLContext context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT, + glAttributes, 0); + Assertions.checkState(context != null, "eglCreateContext failed"); + + int[] pbufferAttributes; + if (secure) { + pbufferAttributes = new int[] { + EGL_WIDTH, 1, + EGL_HEIGHT, 1, + EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, + EGL_NONE}; + } else { + pbufferAttributes = new int[] { + EGL_WIDTH, 1, + EGL_HEIGHT, 1, + EGL_NONE}; + } + EGLSurface pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); + Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); + + boolean eglMadeCurrent = eglMakeCurrent(display, pbuffer, pbuffer, context); + Assertions.checkState(eglMadeCurrent, "eglMakeCurrent failed"); + + glGenTextures(1, textureIdHolder, 0); + surfaceTexture = new SurfaceTexture(textureIdHolder[0]); + surfaceTexture.setOnFrameAvailableListener(this); + surface = new DummySurface(this, surfaceTexture, secure); + } + + private void releaseInternal() { + try { + surfaceTexture.release(); + } finally { + surface = null; + surfaceTexture = null; + glDeleteTextures(1, textureIdHolder, 0); + } + } + + } + +} From b408750aa9daed185be188d9e671103897c34ff2 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 4 May 2017 03:24:19 -0700 Subject: [PATCH 010/220] Propagate EXT-X-DATERANGE tags with media playlists ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155062718 --- .../exoplayer2/source/hls/playlist/HlsMediaPlaylist.java | 9 ++++++--- .../source/hls/playlist/HlsPlaylistParser.java | 7 ++++++- 2 files changed, 12 insertions(+), 4 deletions(-) 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 c7708a1d2f..69b95e6d3d 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 @@ -91,12 +91,14 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public final boolean hasProgramDateTime; public final Segment initializationSegment; public final List segments; + public final List dateRanges; public final long durationUs; public HlsMediaPlaylist(@PlaylistType int playlistType, String baseUri, long startOffsetUs, long startTimeUs, boolean hasDiscontinuitySequence, int discontinuitySequence, int mediaSequence, int version, long targetDurationUs, boolean hasEndTag, - boolean hasProgramDateTime, Segment initializationSegment, List segments) { + boolean hasProgramDateTime, Segment initializationSegment, List segments, + List dateRanges) { super(baseUri); this.playlistType = playlistType; this.startTimeUs = startTimeUs; @@ -117,6 +119,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } this.startOffsetUs = startOffsetUs == C.TIME_UNSET ? C.TIME_UNSET : startOffsetUs >= 0 ? startOffsetUs : durationUs + startOffsetUs; + this.dateRanges = Collections.unmodifiableList(dateRanges); } /** @@ -155,7 +158,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) { return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, true, discontinuitySequence, mediaSequence, version, targetDurationUs, hasEndTag, - hasProgramDateTime, initializationSegment, segments); + hasProgramDateTime, initializationSegment, segments, dateRanges); } /** @@ -170,7 +173,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, hasDiscontinuitySequence, discontinuitySequence, mediaSequence, version, targetDurationUs, - true, hasProgramDateTime, initializationSegment, segments); + true, hasProgramDateTime, initializationSegment, segments, dateRanges); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index d24264cae6..8e01dec6fe 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -57,6 +57,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser segments = new ArrayList<>(); + List dateRanges = new ArrayList<>(); long segmentDurationUs = 0; boolean hasDiscontinuitySequence = false; @@ -343,6 +345,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Thu, 4 May 2017 03:29:10 -0700 Subject: [PATCH 011/220] Fix javadocs typos Issue:#2773 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155062917 --- .../google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 2 +- .../com/google/android/exoplayer2/upstream/cache/CacheUtil.java | 2 +- 2 files changed, 2 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 c44c703bb1..4b629c8d2a 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 @@ -286,7 +286,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { * * @param outputBufferTimeUs The timestamp of the current output buffer. * @param nextOutputBufferTimeUs The timestamp of the next output buffer or - * {@link TIME_UNSET} if the next output buffer is unavailable. + * {@link C#TIME_UNSET} if the next output buffer is unavailable. * @param positionUs The current playback position. * @param joiningDeadlineMs The joining deadline. * @return Returns whether to drop the current output buffer. 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 f6251dbbf1..bb1f88e5ea 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 @@ -64,7 +64,7 @@ public final class CacheUtil { } /** - * Returns already cached and missing bytes in the {@cache} for the data defined by {@code + * Returns already cached and missing bytes in the {@code cache} for the data defined by {@code * dataSpec}. * * @param dataSpec Defines the data to be checked. From 4d1826dd3d079338495a2e5d178c244580c7ad42 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 4 May 2017 06:11:12 -0700 Subject: [PATCH 012/220] Add repeat mode mechanics to Exoplayer. (Relating to GitHub Issue #2577) All getter, setter and callbacks have been added and value of repeatMode is passed to getNextXXXIndex methods. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155071985 --- .../android/exoplayer2/demo/EventLogger.java | 13 +++++++++ .../exoplayer2/demo/PlayerActivity.java | 5 ++++ .../exoplayer2/ext/flac/FlacPlaybackTest.java | 5 ++++ .../exoplayer2/ext/opus/OpusPlaybackTest.java | 5 ++++ .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 5 ++++ .../android/exoplayer2/ExoPlayerTest.java | 5 ++++ .../google/android/exoplayer2/ExoPlayer.java | 23 ++++++++++++++- .../android/exoplayer2/ExoPlayerImpl.java | 20 ++++++++++++- .../exoplayer2/ExoPlayerImplInternal.java | 29 ++++++++++++++----- .../android/exoplayer2/SimpleExoPlayer.java | 10 +++++++ .../exoplayer2/ui/DebugTextViewHelper.java | 5 ++++ .../exoplayer2/ui/PlaybackControlView.java | 15 ++++++---- .../exoplayer2/ui/SimpleExoPlayerView.java | 5 ++++ .../playbacktests/util/ExoHostedTest.java | 5 ++++ 14 files changed, 134 insertions(+), 16 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 953021fe6f..0d77624a7b 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -95,6 +95,11 @@ import java.util.Locale; + getStateString(state) + "]"); } + @Override + public void onRepeatModeChanged(@ExoPlayer.RepeatMode int repeatMode) { + Log.d(TAG, "repeatMode [" + getRepeatModeString(repeatMode) + "]"); + } + @Override public void onPositionDiscontinuity() { Log.d(TAG, "positionDiscontinuity"); @@ -461,4 +466,12 @@ import java.util.Locale; return enabled ? "[X]" : "[ ]"; } + private static String getRepeatModeString(@ExoPlayer.RepeatMode int repeatMode) { + switch (repeatMode) { + case ExoPlayer.REPEAT_MODE_OFF: + return "OFF"; + default: + return "?"; + } + } } diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 2542f23e95..333b3bc42c 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -424,6 +424,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay updateButtonVisibilities(); } + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + @Override public void onPositionDiscontinuity() { if (needRetrySource) { 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 21f01f0cca..a49ae073ef 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 @@ -126,6 +126,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase { } } + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + private void releasePlayerAndQuitLooper() { player.release(); Looper.myLooper().quit(); 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 263934d982..76e19b0ebe 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 @@ -126,6 +126,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase { } } + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + private void releasePlayerAndQuitLooper() { player.release(); Looper.myLooper().quit(); 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 2647776b74..669d77cdeb 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 @@ -158,6 +158,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase { } } + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + private void releasePlayerAndQuitLooper() { player.release(); Looper.myLooper().quit(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 2c10bfe6a0..00eba6b52b 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -340,6 +340,11 @@ public final class ExoPlayerTest extends TestCase { } } + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + @Override public void onTimelineChanged(Timeline timeline, Object manifest) { sourceInfos.add(Pair.create(timeline, manifest)); 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 be04924753..6327ebd9c2 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 @@ -154,6 +154,13 @@ public interface ExoPlayer { */ void onPlayerStateChanged(boolean playWhenReady, int playbackState); + /** + * Called when the value of {@link #getRepeatMode()} changes. + * + * @param repeatMode The {@link RepeatMode} used for playback. + */ + void onRepeatModeChanged(@RepeatMode int repeatMode); + /** * Called when an error occurs. The playback state will transition to {@link #STATE_IDLE} * immediately after this method is called. The player instance can still be used, and @@ -262,7 +269,7 @@ public interface ExoPlayer { */ @Retention(RetentionPolicy.SOURCE) @IntDef({REPEAT_MODE_OFF}) - @interface RepeatMode {} + public @interface RepeatMode {} /** * Normal playback without repetition. */ @@ -328,6 +335,20 @@ public interface ExoPlayer { */ boolean getPlayWhenReady(); + /** + * Sets the {@link RepeatMode} to be used for playback. + * + * @param repeatMode A repeat mode. + */ + void setRepeatMode(@RepeatMode int repeatMode); + + /** + * Returns the current {@link RepeatMode} used for playback. + * + * @return The current repeat mode. + */ + @RepeatMode int getRepeatMode(); + /** * Whether the player is currently loading the source. * 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 cb0958a3b1..94c43167d8 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 @@ -51,6 +51,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private boolean tracksSelected; private boolean playWhenReady; + @RepeatMode int repeatMode; private int playbackState; private int pendingSeekAcks; private int pendingPrepareAcks; @@ -83,6 +84,7 @@ import java.util.concurrent.CopyOnWriteArraySet; this.renderers = Assertions.checkNotNull(renderers); this.trackSelector = Assertions.checkNotNull(trackSelector); this.playWhenReady = false; + this.repeatMode = REPEAT_MODE_OFF; this.playbackState = STATE_IDLE; this.listeners = new CopyOnWriteArraySet<>(); emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]); @@ -101,7 +103,7 @@ import java.util.concurrent.CopyOnWriteArraySet; }; playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0, 0); internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady, - eventHandler, playbackInfo, this); + repeatMode, eventHandler, playbackInfo, this); } @Override @@ -164,6 +166,22 @@ import java.util.concurrent.CopyOnWriteArraySet; return playWhenReady; } + @Override + public void setRepeatMode(@RepeatMode int repeatMode) { + if (this.repeatMode != repeatMode) { + this.repeatMode = repeatMode; + internalPlayer.setRepeatMode(repeatMode); + for (EventListener listener : listeners) { + listener.onRepeatModeChanged(repeatMode); + } + } + } + + @Override + public @RepeatMode int getRepeatMode() { + return repeatMode; + } + @Override public boolean isLoading() { return isLoading; 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 2410e19f04..b8b1314504 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 @@ -112,6 +112,7 @@ import java.io.IOException; private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9; private static final int MSG_TRACK_SELECTION_INVALIDATED = 10; private static final int MSG_CUSTOM = 11; + private static final int MSG_SET_REPEAT_MODE = 12; private static final int PREPARING_SOURCE_INTERVAL_MS = 10; private static final int RENDERING_INTERVAL_MS = 10; @@ -155,6 +156,7 @@ import java.io.IOException; private boolean rebuffering; private boolean isLoading; private int state; + private @ExoPlayer.RepeatMode int repeatMode; private int customMessagesSent; private int customMessagesProcessed; private long elapsedRealtimeUs; @@ -170,12 +172,13 @@ import java.io.IOException; private Timeline timeline; public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector, - LoadControl loadControl, boolean playWhenReady, Handler eventHandler, - PlaybackInfo playbackInfo, ExoPlayer player) { + LoadControl loadControl, boolean playWhenReady, @ExoPlayer.RepeatMode int repeatMode, + Handler eventHandler, PlaybackInfo playbackInfo, ExoPlayer player) { this.renderers = renderers; this.trackSelector = trackSelector; this.loadControl = loadControl; this.playWhenReady = playWhenReady; + this.repeatMode = repeatMode; this.eventHandler = eventHandler; this.state = ExoPlayer.STATE_IDLE; this.playbackInfo = playbackInfo; @@ -210,6 +213,10 @@ import java.io.IOException; handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget(); } + public void setRepeatMode(@ExoPlayer.RepeatMode int repeatMode) { + handler.obtainMessage(MSG_SET_REPEAT_MODE, repeatMode, 0).sendToTarget(); + } + public void seekTo(Timeline timeline, int windowIndex, long positionUs) { handler.obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs)) .sendToTarget(); @@ -304,6 +311,10 @@ import java.io.IOException; setPlayWhenReadyInternal(msg.arg1 != 0); return true; } + case MSG_SET_REPEAT_MODE: { + setRepeatModeInternal(msg.arg1); + return true; + } case MSG_DO_SOME_WORK: { doSomeWork(); return true; @@ -411,6 +422,10 @@ import java.io.IOException; } } + private void setRepeatModeInternal(@ExoPlayer.RepeatMode int repeatMode) { + this.repeatMode = repeatMode; + } + private void startRenderers() throws ExoPlaybackException { rebuffering = false; standaloneMediaClock.start(); @@ -959,8 +974,7 @@ import java.io.IOException; while (periodHolder.next != null) { MediaPeriodHolder previousPeriodHolder = periodHolder; periodHolder = periodHolder.next; - periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, - ExoPlayer.REPEAT_MODE_OFF); + periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode); boolean isLastPeriod = isLastPeriod(periodIndex); timeline.getPeriod(periodIndex, period, true); if (periodHolder.uid.equals(period.uid)) { @@ -1022,8 +1036,7 @@ import java.io.IOException; int newPeriodIndex = C.INDEX_UNSET; int maxIterations = oldTimeline.getPeriodCount(); for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) { - oldPeriodIndex = oldTimeline.getNextPeriodIndex(oldPeriodIndex, period, window, - ExoPlayer.REPEAT_MODE_OFF); + oldPeriodIndex = oldTimeline.getNextPeriodIndex(oldPeriodIndex, period, window, repeatMode); newPeriodIndex = newTimeline.getIndexOfPeriod( oldTimeline.getPeriod(oldPeriodIndex, period, true).uid); } @@ -1033,7 +1046,7 @@ import java.io.IOException; private boolean isLastPeriod(int periodIndex) { int windowIndex = timeline.getPeriod(periodIndex, period).windowIndex; return !timeline.getWindow(windowIndex, window).isDynamic - && timeline.isLastPeriod(periodIndex, period, window, ExoPlayer.REPEAT_MODE_OFF); + && timeline.isLastPeriod(periodIndex, period, window, repeatMode); } /** @@ -1247,7 +1260,7 @@ import java.io.IOException; return; } newLoadingPeriodIndex = timeline.getNextPeriodIndex(loadingPeriodHolder.index, period, window, - ExoPlayer.REPEAT_MODE_OFF); + repeatMode); } if (newLoadingPeriodIndex >= timeline.getPeriodCount()) { 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 6094513913..8dcd390033 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 @@ -514,6 +514,16 @@ public class SimpleExoPlayer implements ExoPlayer { return player.getPlayWhenReady(); } + @Override + public @RepeatMode int getRepeatMode() { + return player.getRepeatMode(); + } + + @Override + public void setRepeatMode(@RepeatMode int repeatMode) { + player.setRepeatMode(repeatMode); + } + @Override public boolean isLoading() { return player.isLoading(); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index 38c7a5be9c..68fa6a8cc9 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -86,6 +86,11 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe updateAndPost(); } + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + @Override public void onPositionDiscontinuity() { updateAndPost(); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index baeada098a..ae5b7c8b13 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -529,10 +529,9 @@ public class PlaybackControlView extends FrameLayout { int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); isSeekable = window.isSeekable; - enablePrevious = !timeline.isFirstWindow(windowIndex, ExoPlayer.REPEAT_MODE_OFF) + enablePrevious = !timeline.isFirstWindow(windowIndex, player.getRepeatMode()) || isSeekable || !window.isDynamic; - enableNext = !timeline.isLastWindow(windowIndex, ExoPlayer.REPEAT_MODE_OFF) - || window.isDynamic; + enableNext = !timeline.isLastWindow(windowIndex, player.getRepeatMode()) || window.isDynamic; if (timeline.getPeriod(player.getCurrentPeriodIndex(), period).isAd) { // Always hide player controls during ads. hide(); @@ -682,8 +681,7 @@ public class PlaybackControlView extends FrameLayout { } int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); - int previousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, - ExoPlayer.REPEAT_MODE_OFF); + int previousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode()); if (previousWindowIndex != C.INDEX_UNSET && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS || (window.isDynamic && !window.isSeekable))) { @@ -699,7 +697,7 @@ public class PlaybackControlView extends FrameLayout { return; } int windowIndex = player.getCurrentWindowIndex(); - int nextWindowIndex = timeline.getNextWindowIndex(windowIndex, ExoPlayer.REPEAT_MODE_OFF); + int nextWindowIndex = timeline.getNextWindowIndex(windowIndex, player.getRepeatMode()); if (nextWindowIndex != C.INDEX_UNSET) { seekTo(nextWindowIndex, C.TIME_UNSET); } else if (timeline.getWindow(windowIndex, window, false).isDynamic) { @@ -908,6 +906,11 @@ public class PlaybackControlView extends FrameLayout { updateProgress(); } + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + @Override public void onPositionDiscontinuity() { updateNavigation(); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index fce05f5bc4..5219778109 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -806,6 +806,11 @@ public final class SimpleExoPlayerView extends FrameLayout { maybeShowController(false); } + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + @Override public void onPlayerError(ExoPlaybackException e) { // Do nothing. diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java index f48318687d..50791d5c83 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java @@ -213,6 +213,11 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen this.playing = playing; } + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + @Override public final void onPlayerError(ExoPlaybackException error) { playerWasPrepared = true; From ba9cbfbb910c0a241da82bc18d940345ddcd8269 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 4 May 2017 07:51:14 -0700 Subject: [PATCH 013/220] Localized strings for repeat modes. Related to GitHub Issue #2577. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155078753 --- library/ui/src/main/res/values-af/strings.xml | 3 +++ library/ui/src/main/res/values-am/strings.xml | 3 +++ library/ui/src/main/res/values-ar/strings.xml | 3 +++ library/ui/src/main/res/values-az-rAZ/strings.xml | 3 +++ library/ui/src/main/res/values-b+sr+Latn/strings.xml | 3 +++ library/ui/src/main/res/values-be-rBY/strings.xml | 3 +++ library/ui/src/main/res/values-bg/strings.xml | 3 +++ library/ui/src/main/res/values-bn-rBD/strings.xml | 3 +++ library/ui/src/main/res/values-bs-rBA/strings.xml | 3 +++ library/ui/src/main/res/values-ca/strings.xml | 3 +++ library/ui/src/main/res/values-cs/strings.xml | 3 +++ library/ui/src/main/res/values-da/strings.xml | 3 +++ library/ui/src/main/res/values-de/strings.xml | 3 +++ library/ui/src/main/res/values-el/strings.xml | 3 +++ library/ui/src/main/res/values-en-rAU/strings.xml | 3 +++ library/ui/src/main/res/values-en-rGB/strings.xml | 3 +++ library/ui/src/main/res/values-en-rIN/strings.xml | 3 +++ library/ui/src/main/res/values-es-rUS/strings.xml | 3 +++ library/ui/src/main/res/values-es/strings.xml | 3 +++ library/ui/src/main/res/values-et-rEE/strings.xml | 3 +++ library/ui/src/main/res/values-eu-rES/strings.xml | 3 +++ library/ui/src/main/res/values-fa/strings.xml | 3 +++ library/ui/src/main/res/values-fi/strings.xml | 3 +++ library/ui/src/main/res/values-fr-rCA/strings.xml | 3 +++ library/ui/src/main/res/values-fr/strings.xml | 3 +++ library/ui/src/main/res/values-gl-rES/strings.xml | 3 +++ library/ui/src/main/res/values-gu-rIN/strings.xml | 3 +++ library/ui/src/main/res/values-hi/strings.xml | 3 +++ library/ui/src/main/res/values-hr/strings.xml | 3 +++ library/ui/src/main/res/values-hu/strings.xml | 3 +++ library/ui/src/main/res/values-hy-rAM/strings.xml | 3 +++ library/ui/src/main/res/values-in/strings.xml | 3 +++ library/ui/src/main/res/values-is-rIS/strings.xml | 3 +++ library/ui/src/main/res/values-it/strings.xml | 3 +++ library/ui/src/main/res/values-iw/strings.xml | 3 +++ library/ui/src/main/res/values-ja/strings.xml | 3 +++ library/ui/src/main/res/values-ka-rGE/strings.xml | 3 +++ library/ui/src/main/res/values-kk-rKZ/strings.xml | 3 +++ library/ui/src/main/res/values-km-rKH/strings.xml | 3 +++ library/ui/src/main/res/values-kn-rIN/strings.xml | 3 +++ library/ui/src/main/res/values-ko/strings.xml | 3 +++ library/ui/src/main/res/values-ky-rKG/strings.xml | 3 +++ library/ui/src/main/res/values-lo-rLA/strings.xml | 3 +++ library/ui/src/main/res/values-lt/strings.xml | 3 +++ library/ui/src/main/res/values-lv/strings.xml | 3 +++ library/ui/src/main/res/values-mk-rMK/strings.xml | 3 +++ library/ui/src/main/res/values-ml-rIN/strings.xml | 3 +++ library/ui/src/main/res/values-mn-rMN/strings.xml | 3 +++ library/ui/src/main/res/values-mr-rIN/strings.xml | 3 +++ library/ui/src/main/res/values-ms-rMY/strings.xml | 3 +++ library/ui/src/main/res/values-my-rMM/strings.xml | 3 +++ library/ui/src/main/res/values-nb/strings.xml | 3 +++ library/ui/src/main/res/values-ne-rNP/strings.xml | 3 +++ library/ui/src/main/res/values-nl/strings.xml | 3 +++ library/ui/src/main/res/values-pa-rIN/strings.xml | 3 +++ library/ui/src/main/res/values-pl/strings.xml | 3 +++ library/ui/src/main/res/values-pt-rBR/strings.xml | 3 +++ library/ui/src/main/res/values-pt-rPT/strings.xml | 3 +++ library/ui/src/main/res/values-pt/strings.xml | 3 +++ library/ui/src/main/res/values-ro/strings.xml | 3 +++ library/ui/src/main/res/values-ru/strings.xml | 3 +++ library/ui/src/main/res/values-si-rLK/strings.xml | 3 +++ library/ui/src/main/res/values-sk/strings.xml | 3 +++ library/ui/src/main/res/values-sl/strings.xml | 3 +++ library/ui/src/main/res/values-sq-rAL/strings.xml | 3 +++ library/ui/src/main/res/values-sv/strings.xml | 3 +++ library/ui/src/main/res/values-sw/strings.xml | 3 +++ library/ui/src/main/res/values-ta-rIN/strings.xml | 3 +++ library/ui/src/main/res/values-te-rIN/strings.xml | 3 +++ library/ui/src/main/res/values-th/strings.xml | 3 +++ library/ui/src/main/res/values-tl/strings.xml | 3 +++ library/ui/src/main/res/values-tr/strings.xml | 3 +++ library/ui/src/main/res/values-uk/strings.xml | 3 +++ library/ui/src/main/res/values-ur-rPK/strings.xml | 3 +++ library/ui/src/main/res/values-uz-rUZ/strings.xml | 3 +++ library/ui/src/main/res/values-vi/strings.xml | 3 +++ library/ui/src/main/res/values-zh-rCN/strings.xml | 3 +++ library/ui/src/main/res/values-zh-rHK/strings.xml | 3 +++ library/ui/src/main/res/values-zh-rTW/strings.xml | 3 +++ library/ui/src/main/res/values-zu/strings.xml | 3 +++ library/ui/src/main/res/values/strings.xml | 3 +++ 81 files changed, 243 insertions(+) diff --git a/library/ui/src/main/res/values-af/strings.xml b/library/ui/src/main/res/values-af/strings.xml index 9f1bce53d9..103877f1e6 100644 --- a/library/ui/src/main/res/values-af/strings.xml +++ b/library/ui/src/main/res/values-af/strings.xml @@ -22,4 +22,7 @@ "Stop" "Spoel terug" "Vinnig vorentoe" + "Herhaal alles" + "Herhaal niks" + "Herhaal een" diff --git a/library/ui/src/main/res/values-am/strings.xml b/library/ui/src/main/res/values-am/strings.xml index f06c2a664e..356566cb87 100644 --- a/library/ui/src/main/res/values-am/strings.xml +++ b/library/ui/src/main/res/values-am/strings.xml @@ -22,4 +22,7 @@ "አቁም" "ወደኋላ አጠንጥን" "በፍጥነት አሳልፍ" + "ሁሉንም ድገም" + "ምንም አትድገም" + "አንዱን ድገም" diff --git a/library/ui/src/main/res/values-ar/strings.xml b/library/ui/src/main/res/values-ar/strings.xml index a40c961bf7..4bdbda061c 100644 --- a/library/ui/src/main/res/values-ar/strings.xml +++ b/library/ui/src/main/res/values-ar/strings.xml @@ -22,4 +22,7 @@ "إيقاف" "إرجاع" "تقديم سريع" + "تكرار الكل" + "عدم التكرار" + "تكرار مقطع واحد" diff --git a/library/ui/src/main/res/values-az-rAZ/strings.xml b/library/ui/src/main/res/values-az-rAZ/strings.xml index 7b3b9366b5..771335952f 100644 --- a/library/ui/src/main/res/values-az-rAZ/strings.xml +++ b/library/ui/src/main/res/values-az-rAZ/strings.xml @@ -22,4 +22,7 @@ "Dayandır" "Geri sarıma" "Sürətlə irəli" + "Bütün təkrarlayın" + "Təkrar bir" + "Heç bir təkrar" diff --git a/library/ui/src/main/res/values-b+sr+Latn/strings.xml b/library/ui/src/main/res/values-b+sr+Latn/strings.xml index b5fdd74402..7c373b5b55 100644 --- a/library/ui/src/main/res/values-b+sr+Latn/strings.xml +++ b/library/ui/src/main/res/values-b+sr+Latn/strings.xml @@ -22,4 +22,7 @@ "Zaustavi" "Premotaj unazad" "Premotaj unapred" + "Ponovi sve" + "Ne ponavljaj nijednu" + "Ponovi jednu" diff --git a/library/ui/src/main/res/values-be-rBY/strings.xml b/library/ui/src/main/res/values-be-rBY/strings.xml index 890c23ebd5..7790a7887f 100644 --- a/library/ui/src/main/res/values-be-rBY/strings.xml +++ b/library/ui/src/main/res/values-be-rBY/strings.xml @@ -22,4 +22,7 @@ "Спыніць" "Перамотка назад" "Перамотка ўперад" + "Паўтарыць усё" + "Паўтараць ні" + "Паўтарыць адзін" diff --git a/library/ui/src/main/res/values-bg/strings.xml b/library/ui/src/main/res/values-bg/strings.xml index 30b905fb8e..ce9e3d6943 100644 --- a/library/ui/src/main/res/values-bg/strings.xml +++ b/library/ui/src/main/res/values-bg/strings.xml @@ -22,4 +22,7 @@ "Спиране" "Превъртане назад" "Превъртане напред" + "Повтаряне на всички" + "Без повтаряне" + "Повтаряне на един елемент" diff --git a/library/ui/src/main/res/values-bn-rBD/strings.xml b/library/ui/src/main/res/values-bn-rBD/strings.xml index ca5d9461d3..5f8ebfa98e 100644 --- a/library/ui/src/main/res/values-bn-rBD/strings.xml +++ b/library/ui/src/main/res/values-bn-rBD/strings.xml @@ -22,4 +22,7 @@ "থামান" "গুটিয়ে নিন" "দ্রুত সামনে এগোন" + "সবগুলির পুনরাবৃত্তি করুন" + "একটিরও পুনরাবৃত্তি করবেন না" + "একটির পুনরাবৃত্তি করুন" diff --git a/library/ui/src/main/res/values-bs-rBA/strings.xml b/library/ui/src/main/res/values-bs-rBA/strings.xml index 9cb0ca4d76..ef47099760 100644 --- a/library/ui/src/main/res/values-bs-rBA/strings.xml +++ b/library/ui/src/main/res/values-bs-rBA/strings.xml @@ -22,4 +22,7 @@ "Zaustavi" "Premotaj" "Ubrzaj" + "Ponovite sve" + "Ne ponavljaju" + "Ponovite jedan" diff --git a/library/ui/src/main/res/values-ca/strings.xml b/library/ui/src/main/res/values-ca/strings.xml index 0816c76b12..a42fe3b9cb 100644 --- a/library/ui/src/main/res/values-ca/strings.xml +++ b/library/ui/src/main/res/values-ca/strings.xml @@ -22,4 +22,7 @@ "Atura" "Rebobina" "Avança ràpidament" + "Repeteix-ho tot" + "No en repeteixis cap" + "Repeteix-ne un" diff --git a/library/ui/src/main/res/values-cs/strings.xml b/library/ui/src/main/res/values-cs/strings.xml index 22cff4041e..9c1e50ce27 100644 --- a/library/ui/src/main/res/values-cs/strings.xml +++ b/library/ui/src/main/res/values-cs/strings.xml @@ -22,4 +22,7 @@ "Zastavit" "Přetočit zpět" "Přetočit vpřed" + "Opakovat vše" + "Neopakovat" + "Opakovat jednu položku" diff --git a/library/ui/src/main/res/values-da/strings.xml b/library/ui/src/main/res/values-da/strings.xml index a6710bea50..3ec132ebb7 100644 --- a/library/ui/src/main/res/values-da/strings.xml +++ b/library/ui/src/main/res/values-da/strings.xml @@ -22,4 +22,7 @@ "Stop" "Spol tilbage" "Spol frem" + "Gentag alle" + "Gentag ingen" + "Gentag en" diff --git a/library/ui/src/main/res/values-de/strings.xml b/library/ui/src/main/res/values-de/strings.xml index cdfd2d4baf..a1dc749864 100644 --- a/library/ui/src/main/res/values-de/strings.xml +++ b/library/ui/src/main/res/values-de/strings.xml @@ -22,4 +22,7 @@ "Beenden" "Zurückspulen" "Vorspulen" + "Alle wiederholen" + "Keinen Titel wiederholen" + "Einen Titel wiederholen" diff --git a/library/ui/src/main/res/values-el/strings.xml b/library/ui/src/main/res/values-el/strings.xml index 1e11df3b14..845011fe55 100644 --- a/library/ui/src/main/res/values-el/strings.xml +++ b/library/ui/src/main/res/values-el/strings.xml @@ -22,4 +22,7 @@ "Διακοπή" "Επαναφορά" "Γρήγορη προώθηση" + "Επανάληψη όλων" + "Καμία επανάληψη" + "Επανάληψη ενός στοιχείου" diff --git a/library/ui/src/main/res/values-en-rAU/strings.xml b/library/ui/src/main/res/values-en-rAU/strings.xml index 5077cf2b94..8a1742c8ca 100644 --- a/library/ui/src/main/res/values-en-rAU/strings.xml +++ b/library/ui/src/main/res/values-en-rAU/strings.xml @@ -22,4 +22,7 @@ "Stop" "Rewind" "Fast-forward" + "Repeat all" + "Repeat none" + "Repeat one" diff --git a/library/ui/src/main/res/values-en-rGB/strings.xml b/library/ui/src/main/res/values-en-rGB/strings.xml index 5077cf2b94..8a1742c8ca 100644 --- a/library/ui/src/main/res/values-en-rGB/strings.xml +++ b/library/ui/src/main/res/values-en-rGB/strings.xml @@ -22,4 +22,7 @@ "Stop" "Rewind" "Fast-forward" + "Repeat all" + "Repeat none" + "Repeat one" diff --git a/library/ui/src/main/res/values-en-rIN/strings.xml b/library/ui/src/main/res/values-en-rIN/strings.xml index 5077cf2b94..8a1742c8ca 100644 --- a/library/ui/src/main/res/values-en-rIN/strings.xml +++ b/library/ui/src/main/res/values-en-rIN/strings.xml @@ -22,4 +22,7 @@ "Stop" "Rewind" "Fast-forward" + "Repeat all" + "Repeat none" + "Repeat one" diff --git a/library/ui/src/main/res/values-es-rUS/strings.xml b/library/ui/src/main/res/values-es-rUS/strings.xml index 72b176e538..f2ec848fb6 100644 --- a/library/ui/src/main/res/values-es-rUS/strings.xml +++ b/library/ui/src/main/res/values-es-rUS/strings.xml @@ -22,4 +22,7 @@ "Detener" "Retroceder" "Avanzar" + "Repetir todo" + "No repetir" + "Repetir uno" diff --git a/library/ui/src/main/res/values-es/strings.xml b/library/ui/src/main/res/values-es/strings.xml index 3b188d266d..116f064223 100644 --- a/library/ui/src/main/res/values-es/strings.xml +++ b/library/ui/src/main/res/values-es/strings.xml @@ -22,4 +22,7 @@ "Detener" "Rebobinar" "Avance rápido" + "Repetir todo" + "No repetir" + "Repetir uno" diff --git a/library/ui/src/main/res/values-et-rEE/strings.xml b/library/ui/src/main/res/values-et-rEE/strings.xml index 7a01bd9d5a..153611ece4 100644 --- a/library/ui/src/main/res/values-et-rEE/strings.xml +++ b/library/ui/src/main/res/values-et-rEE/strings.xml @@ -22,4 +22,7 @@ "Peata" "Keri tagasi" "Keri edasi" + "Korda kõike" + "Ära korda midagi" + "Korda ühte" diff --git a/library/ui/src/main/res/values-eu-rES/strings.xml b/library/ui/src/main/res/values-eu-rES/strings.xml index 3dd51d2138..1128572d9a 100644 --- a/library/ui/src/main/res/values-eu-rES/strings.xml +++ b/library/ui/src/main/res/values-eu-rES/strings.xml @@ -22,4 +22,7 @@ "Gelditu" "Atzeratu" "Aurreratu" + "Errepikatu guztiak" + "Ez errepikatu" + "Errepikatu bat" diff --git a/library/ui/src/main/res/values-fa/strings.xml b/library/ui/src/main/res/values-fa/strings.xml index a8955ca2f3..d6be77323b 100644 --- a/library/ui/src/main/res/values-fa/strings.xml +++ b/library/ui/src/main/res/values-fa/strings.xml @@ -22,4 +22,7 @@ "توقف" "عقب بردن" "جلو بردن سریع" + "تکرار همه" + "تکرار هیچ‌کدام" + "یک‌بار تکرار" diff --git a/library/ui/src/main/res/values-fi/strings.xml b/library/ui/src/main/res/values-fi/strings.xml index 5f1352d1af..10e4b0bbe3 100644 --- a/library/ui/src/main/res/values-fi/strings.xml +++ b/library/ui/src/main/res/values-fi/strings.xml @@ -22,4 +22,7 @@ "Seis" "Kelaa taakse" "Kelaa eteen" + "Toista kaikki" + "Toista ei mitään" + "Toista yksi" diff --git a/library/ui/src/main/res/values-fr-rCA/strings.xml b/library/ui/src/main/res/values-fr-rCA/strings.xml index 51ba11e0c0..d8852b5d3f 100644 --- a/library/ui/src/main/res/values-fr-rCA/strings.xml +++ b/library/ui/src/main/res/values-fr-rCA/strings.xml @@ -22,4 +22,7 @@ "Arrêter" "Reculer" "Avance rapide" + "Tout lire en boucle" + "Aucune répétition" + "Répéter un élément" diff --git a/library/ui/src/main/res/values-fr/strings.xml b/library/ui/src/main/res/values-fr/strings.xml index d55b32b6f7..acf3670fa4 100644 --- a/library/ui/src/main/res/values-fr/strings.xml +++ b/library/ui/src/main/res/values-fr/strings.xml @@ -22,4 +22,7 @@ "Arrêter" "Retour arrière" "Avance rapide" + "Tout lire en boucle" + "Ne rien lire en boucle" + "Lire en boucle un élément" diff --git a/library/ui/src/main/res/values-gl-rES/strings.xml b/library/ui/src/main/res/values-gl-rES/strings.xml index 99ae59c7f9..81b854cafe 100644 --- a/library/ui/src/main/res/values-gl-rES/strings.xml +++ b/library/ui/src/main/res/values-gl-rES/strings.xml @@ -22,4 +22,7 @@ "Deter" "Rebobinar" "Avance rápido" + "Repetir todo" + "Non repetir" + "Repetir un" diff --git a/library/ui/src/main/res/values-gu-rIN/strings.xml b/library/ui/src/main/res/values-gu-rIN/strings.xml index 6feab0a3a6..6d51c29f97 100644 --- a/library/ui/src/main/res/values-gu-rIN/strings.xml +++ b/library/ui/src/main/res/values-gu-rIN/strings.xml @@ -22,4 +22,7 @@ "રોકો" "રીવાઇન્ડ કરો" "ઝડપી ફોરવર્ડ કરો" + "બધા પુનરાવર્તન કરો" + "કંઈ પુનરાવર્તન કરો" + "એક પુનરાવર્તન કરો" diff --git a/library/ui/src/main/res/values-hi/strings.xml b/library/ui/src/main/res/values-hi/strings.xml index 5229b67d0e..eadb0519df 100644 --- a/library/ui/src/main/res/values-hi/strings.xml +++ b/library/ui/src/main/res/values-hi/strings.xml @@ -22,4 +22,7 @@ "बंद करें" "रिवाइंड करें" "फ़ास्ट फ़ॉरवर्ड" + "सभी को दोहराएं" + "कुछ भी न दोहराएं" + "एक दोहराएं" diff --git a/library/ui/src/main/res/values-hr/strings.xml b/library/ui/src/main/res/values-hr/strings.xml index c0b075edde..cb49965640 100644 --- a/library/ui/src/main/res/values-hr/strings.xml +++ b/library/ui/src/main/res/values-hr/strings.xml @@ -22,4 +22,7 @@ "Zaustavi" "Unatrag" "Brzo unaprijed" + "Ponovi sve" + "Bez ponavljanja" + "Ponovi jedno" diff --git a/library/ui/src/main/res/values-hu/strings.xml b/library/ui/src/main/res/values-hu/strings.xml index 2a34684edb..43ac8f51ff 100644 --- a/library/ui/src/main/res/values-hu/strings.xml +++ b/library/ui/src/main/res/values-hu/strings.xml @@ -22,4 +22,7 @@ "Leállítás" "Visszatekerés" "Előretekerés" + "Összes ismétlése" + "Nincs ismétlés" + "Egy ismétlése" diff --git a/library/ui/src/main/res/values-hy-rAM/strings.xml b/library/ui/src/main/res/values-hy-rAM/strings.xml index 05f9d04ab7..3b09f9a507 100644 --- a/library/ui/src/main/res/values-hy-rAM/strings.xml +++ b/library/ui/src/main/res/values-hy-rAM/strings.xml @@ -22,4 +22,7 @@ "Դադարեցնել" "Հետ փաթաթել" "Արագ առաջ անցնել" + "կրկնել այն ամենը" + "Չկրկնել" + "Կրկնել մեկը" diff --git a/library/ui/src/main/res/values-in/strings.xml b/library/ui/src/main/res/values-in/strings.xml index 062933a0a8..928be5945a 100644 --- a/library/ui/src/main/res/values-in/strings.xml +++ b/library/ui/src/main/res/values-in/strings.xml @@ -22,4 +22,7 @@ "Berhenti" "Putar Ulang" "Maju cepat" + "Ulangi Semua" + "Jangan Ulangi" + "Ulangi Satu" diff --git a/library/ui/src/main/res/values-is-rIS/strings.xml b/library/ui/src/main/res/values-is-rIS/strings.xml index 9c4421a272..75be2aeb17 100644 --- a/library/ui/src/main/res/values-is-rIS/strings.xml +++ b/library/ui/src/main/res/values-is-rIS/strings.xml @@ -22,4 +22,7 @@ "Stöðva" "Spóla til baka" "Spóla áfram" + "Endurtaka allt" + "Endurtaka ekkert" + "Endurtaka eitt" diff --git a/library/ui/src/main/res/values-it/strings.xml b/library/ui/src/main/res/values-it/strings.xml index 71525a2b3e..59117a6b75 100644 --- a/library/ui/src/main/res/values-it/strings.xml +++ b/library/ui/src/main/res/values-it/strings.xml @@ -22,4 +22,7 @@ "Interrompi" "Riavvolgi" "Avanti veloce" + "Ripeti tutti" + "Non ripetere nessuno" + "Ripeti uno" diff --git a/library/ui/src/main/res/values-iw/strings.xml b/library/ui/src/main/res/values-iw/strings.xml index f33cc2adb0..347b137cf2 100644 --- a/library/ui/src/main/res/values-iw/strings.xml +++ b/library/ui/src/main/res/values-iw/strings.xml @@ -22,4 +22,7 @@ "הפסק" "הרץ אחורה" "הרץ קדימה" + "חזור על הכל" + "אל תחזור על כלום" + "חזור על פריט אחד" diff --git a/library/ui/src/main/res/values-ja/strings.xml b/library/ui/src/main/res/values-ja/strings.xml index baa459aeca..cf2cc49b67 100644 --- a/library/ui/src/main/res/values-ja/strings.xml +++ b/library/ui/src/main/res/values-ja/strings.xml @@ -22,4 +22,7 @@ "停止" "巻き戻し" "早送り" + "全曲を繰り返し" + "繰り返しなし" + "1曲を繰り返し" diff --git a/library/ui/src/main/res/values-ka-rGE/strings.xml b/library/ui/src/main/res/values-ka-rGE/strings.xml index 5b87f86c34..75da8dde18 100644 --- a/library/ui/src/main/res/values-ka-rGE/strings.xml +++ b/library/ui/src/main/res/values-ka-rGE/strings.xml @@ -22,4 +22,7 @@ "შეწყვეტა" "უკან გადახვევა" "წინ გადახვევა" + "გამეორება ყველა" + "გაიმეორეთ არცერთი" + "გაიმეორეთ ერთი" diff --git a/library/ui/src/main/res/values-kk-rKZ/strings.xml b/library/ui/src/main/res/values-kk-rKZ/strings.xml index c1bf5c8b4b..b1ab22ecf6 100644 --- a/library/ui/src/main/res/values-kk-rKZ/strings.xml +++ b/library/ui/src/main/res/values-kk-rKZ/strings.xml @@ -22,4 +22,7 @@ "Тоқтату" "Кері айналдыру" "Жылдам алға айналдыру" + "Барлығын қайталау" + "Ешқайсысын қайталамау" + "Біреуін қайталау" diff --git a/library/ui/src/main/res/values-km-rKH/strings.xml b/library/ui/src/main/res/values-km-rKH/strings.xml index dbeeab60a6..dfd9f7d863 100644 --- a/library/ui/src/main/res/values-km-rKH/strings.xml +++ b/library/ui/src/main/res/values-km-rKH/strings.xml @@ -22,4 +22,7 @@ "បញ្ឈប់" "ខា​ថយក្រោយ" "ទៅ​មុខ​​​រហ័ស" + "ធ្វើ​ម្ដង​ទៀត​ទាំងអស់" + "មិន​ធ្វើ​ឡើង​វិញ" + "ធ្វើ​​ឡើងវិញ​ម្ដង" diff --git a/library/ui/src/main/res/values-kn-rIN/strings.xml b/library/ui/src/main/res/values-kn-rIN/strings.xml index b73cf0fdb0..868af17a65 100644 --- a/library/ui/src/main/res/values-kn-rIN/strings.xml +++ b/library/ui/src/main/res/values-kn-rIN/strings.xml @@ -22,4 +22,7 @@ "ನಿಲ್ಲಿಸು" "ರಿವೈಂಡ್ ಮಾಡು" "ವೇಗವಾಗಿ ಮುಂದಕ್ಕೆ" + "ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ" + "ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ" + "ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ" diff --git a/library/ui/src/main/res/values-ko/strings.xml b/library/ui/src/main/res/values-ko/strings.xml index 7097e2d9f7..89636ac8a0 100644 --- a/library/ui/src/main/res/values-ko/strings.xml +++ b/library/ui/src/main/res/values-ko/strings.xml @@ -22,4 +22,7 @@ "중지" "되감기" "빨리 감기" + "전체 반복" + "반복 안함" + "한 항목 반복" diff --git a/library/ui/src/main/res/values-ky-rKG/strings.xml b/library/ui/src/main/res/values-ky-rKG/strings.xml index 7090c178c3..15fd50468a 100644 --- a/library/ui/src/main/res/values-ky-rKG/strings.xml +++ b/library/ui/src/main/res/values-ky-rKG/strings.xml @@ -22,4 +22,7 @@ "Токтотуу" "Артка түрүү" "Алдыга түрүү" + "Баарын кайталоо" + "Эч бирин кайталабоо" + "Бирөөнү кайталоо" diff --git a/library/ui/src/main/res/values-lo-rLA/strings.xml b/library/ui/src/main/res/values-lo-rLA/strings.xml index 44095e4323..405d0c64fe 100644 --- a/library/ui/src/main/res/values-lo-rLA/strings.xml +++ b/library/ui/src/main/res/values-lo-rLA/strings.xml @@ -22,4 +22,7 @@ "ຢຸດ" "​ຣີ​​ວາຍກັບ" "ເລື່ອນ​ໄປ​ໜ້າ" + "ຫຼິ້ນ​ຊ້ຳ​ທັງ​ໝົດ" + "​ບໍ່ຫຼິ້ນ​ຊ້ຳ" + "ຫຼິ້ນ​ຊ້ຳ" diff --git a/library/ui/src/main/res/values-lt/strings.xml b/library/ui/src/main/res/values-lt/strings.xml index 138caec322..bd7d4142fc 100644 --- a/library/ui/src/main/res/values-lt/strings.xml +++ b/library/ui/src/main/res/values-lt/strings.xml @@ -22,4 +22,7 @@ "Stabdyti" "Sukti atgal" "Sukti pirmyn" + "Kartoti viską" + "Nekartoti nieko" + "Kartoti vieną" diff --git a/library/ui/src/main/res/values-lv/strings.xml b/library/ui/src/main/res/values-lv/strings.xml index 4c91da86cc..c2ebc70cbd 100644 --- a/library/ui/src/main/res/values-lv/strings.xml +++ b/library/ui/src/main/res/values-lv/strings.xml @@ -22,4 +22,7 @@ "Apturēt" "Attīt atpakaļ" "Ātri patīt" + "Atkārtot visu" + "Neatkārtot nevienu" + "Atkārtot vienu" diff --git a/library/ui/src/main/res/values-mk-rMK/strings.xml b/library/ui/src/main/res/values-mk-rMK/strings.xml index e9fedf689f..14ce7111a4 100644 --- a/library/ui/src/main/res/values-mk-rMK/strings.xml +++ b/library/ui/src/main/res/values-mk-rMK/strings.xml @@ -22,4 +22,7 @@ "Запри" "Премотај назад" "Брзо премотај напред" + "Повтори ги сите" + "Не повторувај ниту една" + "Повтори една" diff --git a/library/ui/src/main/res/values-ml-rIN/strings.xml b/library/ui/src/main/res/values-ml-rIN/strings.xml index acc33934fb..17fe7a1655 100644 --- a/library/ui/src/main/res/values-ml-rIN/strings.xml +++ b/library/ui/src/main/res/values-ml-rIN/strings.xml @@ -22,4 +22,7 @@ "നിര്‍ത്തുക" "റിവൈൻഡുചെയ്യുക" "വേഗത്തിലുള്ള കൈമാറൽ" + "എല്ലാം ആവർത്തിക്കുക" + "ഒന്നും ആവർത്തിക്കരുത്" + "ഒന്ന് ആവർത്തിക്കുക" diff --git a/library/ui/src/main/res/values-mn-rMN/strings.xml b/library/ui/src/main/res/values-mn-rMN/strings.xml index 6434e9ea16..bf9a7e03bf 100644 --- a/library/ui/src/main/res/values-mn-rMN/strings.xml +++ b/library/ui/src/main/res/values-mn-rMN/strings.xml @@ -22,4 +22,7 @@ "Зогсоох" "Буцааж хураах" "Хурдан урагшлуулах" + "Бүгдийг давтах" + "Алийг нь ч давтахгүй" + "Нэгийг давтах" diff --git a/library/ui/src/main/res/values-mr-rIN/strings.xml b/library/ui/src/main/res/values-mr-rIN/strings.xml index 8f4d0d75b1..df4ac9de6b 100644 --- a/library/ui/src/main/res/values-mr-rIN/strings.xml +++ b/library/ui/src/main/res/values-mr-rIN/strings.xml @@ -22,4 +22,7 @@ "थांबा" "रिवाईँड करा" "फास्ट फॉरवर्ड करा" + "सर्व पुनरावृत्ती करा" + "काहीही पुनरावृत्ती करू नका" + "एक पुनरावृत्ती करा" diff --git a/library/ui/src/main/res/values-ms-rMY/strings.xml b/library/ui/src/main/res/values-ms-rMY/strings.xml index 91f74bbc1c..33dfcb40f0 100644 --- a/library/ui/src/main/res/values-ms-rMY/strings.xml +++ b/library/ui/src/main/res/values-ms-rMY/strings.xml @@ -22,4 +22,7 @@ "Berhenti" "Gulung semula" "Mara laju" + "Ulang semua" + "Tiada ulangan" + "Ulangan" diff --git a/library/ui/src/main/res/values-my-rMM/strings.xml b/library/ui/src/main/res/values-my-rMM/strings.xml index 4b68e6e950..b4ea5b1155 100644 --- a/library/ui/src/main/res/values-my-rMM/strings.xml +++ b/library/ui/src/main/res/values-my-rMM/strings.xml @@ -22,4 +22,7 @@ "ရပ်ရန်" "ပြန်ရစ်ရန်" "ရှေ့သို့ သွားရန်" + "အားလုံး ထပ်တလဲလဲဖွင့်ရန်" + "ထပ်တလဲလဲမဖွင့်ရန်" + "တစ်ခုအား ထပ်တလဲလဲဖွင့်ရန်" diff --git a/library/ui/src/main/res/values-nb/strings.xml b/library/ui/src/main/res/values-nb/strings.xml index 37454235ad..679bf1134c 100644 --- a/library/ui/src/main/res/values-nb/strings.xml +++ b/library/ui/src/main/res/values-nb/strings.xml @@ -22,4 +22,7 @@ "Stopp" "Tilbakespoling" "Fremoverspoling" + "Gjenta alle" + "Ikke gjenta noen" + "Gjenta én" diff --git a/library/ui/src/main/res/values-ne-rNP/strings.xml b/library/ui/src/main/res/values-ne-rNP/strings.xml index 375e44afce..43730c1880 100644 --- a/library/ui/src/main/res/values-ne-rNP/strings.xml +++ b/library/ui/src/main/res/values-ne-rNP/strings.xml @@ -22,4 +22,7 @@ "रोक्नुहोस्" "दोहोर्याउनुहोस्" "फास्ट फर्वार्ड" + "सबै दोहोर्याउनुहोस्" + "कुनै पनि नदोहोर्याउनुहोस्" + "एउटा दोहोर्याउनुहोस्" diff --git a/library/ui/src/main/res/values-nl/strings.xml b/library/ui/src/main/res/values-nl/strings.xml index 2bdbf0bdae..6383c977fc 100644 --- a/library/ui/src/main/res/values-nl/strings.xml +++ b/library/ui/src/main/res/values-nl/strings.xml @@ -22,4 +22,7 @@ "Stoppen" "Terugspoelen" "Vooruitspoelen" + "Alles herhalen" + "Niet herhalen" + "Eén herhalen" diff --git a/library/ui/src/main/res/values-pa-rIN/strings.xml b/library/ui/src/main/res/values-pa-rIN/strings.xml index 143508e071..ddf60b0394 100644 --- a/library/ui/src/main/res/values-pa-rIN/strings.xml +++ b/library/ui/src/main/res/values-pa-rIN/strings.xml @@ -22,4 +22,7 @@ "ਰੋਕੋ" "ਰੀਵਾਈਂਡ ਕਰੋ" "ਅੱਗੇ ਭੇਜੋ" + "ਸਭ ਨੂੰ ਦੁਹਰਾਓ" + "ਕੋਈ ਵੀ ਨਹੀਂ ਦੁਹਰਾਓ" + "ਇੱਕ ਦੁਹਰਾਓ" diff --git a/library/ui/src/main/res/values-pl/strings.xml b/library/ui/src/main/res/values-pl/strings.xml index 64f52d5d09..113c568f85 100644 --- a/library/ui/src/main/res/values-pl/strings.xml +++ b/library/ui/src/main/res/values-pl/strings.xml @@ -22,4 +22,7 @@ "Zatrzymaj" "Przewiń do tyłu" "Przewiń do przodu" + "Powtórz wszystkie" + "Nie powtarzaj" + "Powtórz jeden" diff --git a/library/ui/src/main/res/values-pt-rBR/strings.xml b/library/ui/src/main/res/values-pt-rBR/strings.xml index 51bcf4d723..87c54358ba 100644 --- a/library/ui/src/main/res/values-pt-rBR/strings.xml +++ b/library/ui/src/main/res/values-pt-rBR/strings.xml @@ -22,4 +22,7 @@ "Parar" "Retroceder" "Avançar" + "Repetir tudo" + "Não repetir" + "Repetir um" diff --git a/library/ui/src/main/res/values-pt-rPT/strings.xml b/library/ui/src/main/res/values-pt-rPT/strings.xml index 5b3c9131d0..ca34afec3c 100644 --- a/library/ui/src/main/res/values-pt-rPT/strings.xml +++ b/library/ui/src/main/res/values-pt-rPT/strings.xml @@ -22,4 +22,7 @@ "Parar" "Rebobinar" "Avançar" + "Repetir tudo" + "Não repetir" + "Repetir um" diff --git a/library/ui/src/main/res/values-pt/strings.xml b/library/ui/src/main/res/values-pt/strings.xml index 51bcf4d723..2fc3191738 100644 --- a/library/ui/src/main/res/values-pt/strings.xml +++ b/library/ui/src/main/res/values-pt/strings.xml @@ -22,4 +22,7 @@ "Parar" "Retroceder" "Avançar" + "Repetir tudo" + "Não repetir" + "Repetir uma" diff --git a/library/ui/src/main/res/values-ro/strings.xml b/library/ui/src/main/res/values-ro/strings.xml index 5a7feda78c..0b2ce540f7 100644 --- a/library/ui/src/main/res/values-ro/strings.xml +++ b/library/ui/src/main/res/values-ro/strings.xml @@ -22,4 +22,7 @@ "Opriți" "Derulați" "Derulați rapid înainte" + "Repetați toate" + "Repetați niciuna" + "Repetați unul" diff --git a/library/ui/src/main/res/values-ru/strings.xml b/library/ui/src/main/res/values-ru/strings.xml index da47546a8b..bfff499a0a 100644 --- a/library/ui/src/main/res/values-ru/strings.xml +++ b/library/ui/src/main/res/values-ru/strings.xml @@ -22,4 +22,7 @@ "Остановить" "Перемотать назад" "Перемотать вперед" + "Повторять все." + "Не повторять." + "Повторять один элемент." diff --git a/library/ui/src/main/res/values-si-rLK/strings.xml b/library/ui/src/main/res/values-si-rLK/strings.xml index 0b579240e8..bc37d98eed 100644 --- a/library/ui/src/main/res/values-si-rLK/strings.xml +++ b/library/ui/src/main/res/values-si-rLK/strings.xml @@ -22,4 +22,7 @@ "නතර කරන්න" "නැවත ඔතන්න" "වේගයෙන් ඉදිරියට යන" + "සියලු නැවත" + "කිසිවක් නැවත" + "නැවත නැවත එක්" diff --git a/library/ui/src/main/res/values-sk/strings.xml b/library/ui/src/main/res/values-sk/strings.xml index 7596497e06..a6ea26bdf0 100644 --- a/library/ui/src/main/res/values-sk/strings.xml +++ b/library/ui/src/main/res/values-sk/strings.xml @@ -22,4 +22,7 @@ "Zastaviť" "Pretočiť späť" "Pretočiť dopredu" + "Opakovať všetko" + "Neopakovať" + "Opakovať jednu položku" diff --git a/library/ui/src/main/res/values-sl/strings.xml b/library/ui/src/main/res/values-sl/strings.xml index a77586b50c..39813fa385 100644 --- a/library/ui/src/main/res/values-sl/strings.xml +++ b/library/ui/src/main/res/values-sl/strings.xml @@ -22,4 +22,7 @@ "Ustavi" "Previj nazaj" "Previj naprej" + "Ponovi vse" + "Ne ponovi" + "Ponovi eno" diff --git a/library/ui/src/main/res/values-sq-rAL/strings.xml b/library/ui/src/main/res/values-sq-rAL/strings.xml index 1fb824366d..0bdc2e5f84 100644 --- a/library/ui/src/main/res/values-sq-rAL/strings.xml +++ b/library/ui/src/main/res/values-sq-rAL/strings.xml @@ -22,4 +22,7 @@ "Ndalo" "Kthehu pas" "Përparo me shpejtësi" + "Përsërit të gjithë" + "Përsëritni asnjë" + "Përsëritni një" diff --git a/library/ui/src/main/res/values-sv/strings.xml b/library/ui/src/main/res/values-sv/strings.xml index e6a8960458..0f7f16f91d 100644 --- a/library/ui/src/main/res/values-sv/strings.xml +++ b/library/ui/src/main/res/values-sv/strings.xml @@ -22,4 +22,7 @@ "Avbryt" "Spola tillbaka" "Snabbspola framåt" + "Upprepa alla" + "Upprepa inga" + "Upprepa en" diff --git a/library/ui/src/main/res/values-sw/strings.xml b/library/ui/src/main/res/values-sw/strings.xml index 8055b7daff..b48af88659 100644 --- a/library/ui/src/main/res/values-sw/strings.xml +++ b/library/ui/src/main/res/values-sw/strings.xml @@ -22,4 +22,7 @@ "Simamisha" "Rudisha nyuma" "Peleka mbele kwa kasi" + "Rudia zote" + "Usirudie Yoyote" + "Rudia Moja" diff --git a/library/ui/src/main/res/values-ta-rIN/strings.xml b/library/ui/src/main/res/values-ta-rIN/strings.xml index 3eb995d467..3dd64f52f7 100644 --- a/library/ui/src/main/res/values-ta-rIN/strings.xml +++ b/library/ui/src/main/res/values-ta-rIN/strings.xml @@ -22,4 +22,7 @@ "நிறுத்து" "மீண்டும் காட்டு" "வேகமாக முன்செல்" + "அனைத்தையும் மீண்டும் இயக்கு" + "எதையும் மீண்டும் இயக்காதே" + "ஒன்றை மட்டும் மீண்டும் இயக்கு" diff --git a/library/ui/src/main/res/values-te-rIN/strings.xml b/library/ui/src/main/res/values-te-rIN/strings.xml index fe7930455a..daf337a931 100644 --- a/library/ui/src/main/res/values-te-rIN/strings.xml +++ b/library/ui/src/main/res/values-te-rIN/strings.xml @@ -22,4 +22,7 @@ "ఆపివేయి" "రివైండ్ చేయి" "వేగంగా ఫార్వార్డ్ చేయి" + "అన్నీ పునరావృతం చేయి" + "ఏదీ పునరావృతం చేయవద్దు" + "ఒకదాన్ని పునరావృతం చేయి" diff --git a/library/ui/src/main/res/values-th/strings.xml b/library/ui/src/main/res/values-th/strings.xml index deb2aac87d..ff89b8d5f5 100644 --- a/library/ui/src/main/res/values-th/strings.xml +++ b/library/ui/src/main/res/values-th/strings.xml @@ -22,4 +22,7 @@ "หยุด" "กรอกลับ" "กรอไปข้างหน้า" + "เล่นซ้ำทั้งหมด" + "ไม่เล่นซ้ำ" + "เล่นซ้ำรายการเดียว" diff --git a/library/ui/src/main/res/values-tl/strings.xml b/library/ui/src/main/res/values-tl/strings.xml index 28dcb3267e..89cf2ef400 100644 --- a/library/ui/src/main/res/values-tl/strings.xml +++ b/library/ui/src/main/res/values-tl/strings.xml @@ -22,4 +22,7 @@ "Ihinto" "I-rewind" "I-fast forward" + "Ulitin Lahat" + "Walang Uulitin" + "Ulitin ang Isa" diff --git a/library/ui/src/main/res/values-tr/strings.xml b/library/ui/src/main/res/values-tr/strings.xml index 4265d796fe..87dba7204c 100644 --- a/library/ui/src/main/res/values-tr/strings.xml +++ b/library/ui/src/main/res/values-tr/strings.xml @@ -22,4 +22,7 @@ "Durdur" "Geri sar" "İleri sar" + "Tümünü Tekrarla" + "Hiçbirini Tekrarlama" + "Birini Tekrarla" diff --git a/library/ui/src/main/res/values-uk/strings.xml b/library/ui/src/main/res/values-uk/strings.xml index 487ca07556..1fdfe2bce5 100644 --- a/library/ui/src/main/res/values-uk/strings.xml +++ b/library/ui/src/main/res/values-uk/strings.xml @@ -22,4 +22,7 @@ "Зупинити" "Перемотати назад" "Перемотати вперед" + "Повторити все" + "Не повторювати" + "Повторити один елемент" diff --git a/library/ui/src/main/res/values-ur-rPK/strings.xml b/library/ui/src/main/res/values-ur-rPK/strings.xml index 55fa908bcd..956374b26a 100644 --- a/library/ui/src/main/res/values-ur-rPK/strings.xml +++ b/library/ui/src/main/res/values-ur-rPK/strings.xml @@ -22,4 +22,7 @@ "روکیں" "ریوائینڈ کریں" "تیزی سے فارورڈ کریں" + "سبھی کو دہرائیں" + "کسی کو نہ دہرائیں" + "ایک کو دہرائیں" diff --git a/library/ui/src/main/res/values-uz-rUZ/strings.xml b/library/ui/src/main/res/values-uz-rUZ/strings.xml index 9cee926844..286d4d01ab 100644 --- a/library/ui/src/main/res/values-uz-rUZ/strings.xml +++ b/library/ui/src/main/res/values-uz-rUZ/strings.xml @@ -22,4 +22,7 @@ "To‘xtatish" "Orqaga o‘tkazish" "Oldinga o‘tkazish" + "Barchasini takrorlash" + "Takrorlamaslik" + "Bir marta takrorlash" diff --git a/library/ui/src/main/res/values-vi/strings.xml b/library/ui/src/main/res/values-vi/strings.xml index 917ec8e95c..4dea58d494 100644 --- a/library/ui/src/main/res/values-vi/strings.xml +++ b/library/ui/src/main/res/values-vi/strings.xml @@ -22,4 +22,7 @@ "Ngừng" "Tua lại" "Tua đi" + "Lặp lại tất cả" + "Không lặp lại" + "Lặp lại một mục" diff --git a/library/ui/src/main/res/values-zh-rCN/strings.xml b/library/ui/src/main/res/values-zh-rCN/strings.xml index 41e02409e2..e15d84e777 100644 --- a/library/ui/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui/src/main/res/values-zh-rCN/strings.xml @@ -22,4 +22,7 @@ "停止" "快退" "快进" + "重复播放全部" + "不重复播放" + "重复播放单个视频" diff --git a/library/ui/src/main/res/values-zh-rHK/strings.xml b/library/ui/src/main/res/values-zh-rHK/strings.xml index a3244bcd70..ba793e98a8 100644 --- a/library/ui/src/main/res/values-zh-rHK/strings.xml +++ b/library/ui/src/main/res/values-zh-rHK/strings.xml @@ -22,4 +22,7 @@ "停止" "倒帶" "向前快轉" + "重複播放所有媒體項目" + "不重複播放任何媒體項目" + "重複播放一個媒體項目" diff --git a/library/ui/src/main/res/values-zh-rTW/strings.xml b/library/ui/src/main/res/values-zh-rTW/strings.xml index ee915c5d9d..bf3364d5cf 100644 --- a/library/ui/src/main/res/values-zh-rTW/strings.xml +++ b/library/ui/src/main/res/values-zh-rTW/strings.xml @@ -22,4 +22,7 @@ "停止" "倒轉" "快轉" + "重複播放所有媒體項目" + "不重複播放" + "重複播放單一媒體項目" diff --git a/library/ui/src/main/res/values-zu/strings.xml b/library/ui/src/main/res/values-zu/strings.xml index e998846454..d7bebaaa2a 100644 --- a/library/ui/src/main/res/values-zu/strings.xml +++ b/library/ui/src/main/res/values-zu/strings.xml @@ -22,4 +22,7 @@ "Misa" "Buyisela emumva" "Ukudlulisa ngokushesha" + "Phinda konke" + "Ungaphindi lutho" + "Phida okukodwa" diff --git a/library/ui/src/main/res/values/strings.xml b/library/ui/src/main/res/values/strings.xml index 1e652dddb3..c5d11eeadb 100644 --- a/library/ui/src/main/res/values/strings.xml +++ b/library/ui/src/main/res/values/strings.xml @@ -21,4 +21,7 @@ Stop Rewind Fast forward + Repeat none + Repeat one + Repeat all From 4c39627ed16b0047b644eee3c9725785f86c080a Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 May 2017 11:30:25 -0700 Subject: [PATCH 014/220] Some minor cleanup related to track selection and caching ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155103828 --- .../exoplayer2/demo/PlayerActivity.java | 6 ++-- .../trackselection/DefaultTrackSelector.java | 16 ++++++++-- .../upstream/cache/CacheDataSource.java | 30 ++++++++++++++----- .../cache/CacheDataSourceFactory.java | 18 +++++++---- 4 files changed, 53 insertions(+), 17 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 333b3bc42c..6cc9cabfc0 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -261,10 +261,10 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this, drmSessionManager, extensionRendererMode); - TrackSelection.Factory videoTrackSelectionFactory = + TrackSelection.Factory adaptiveTrackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); - trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); - trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory); + trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory); + trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory); lastSeenTrackGroupArray = null; player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); 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 941df66e4d..361fcf0b57 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 @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; @@ -376,10 +377,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final AtomicReference paramsReference; /** - * Constructs an instance that does not support adaptive tracks. + * Constructs an instance that does not support adaptive track selection. */ public DefaultTrackSelector() { - this(null); + this((TrackSelection.Factory) null); + } + + /** + * Constructs an instance that supports adaptive track selection. Adaptive track selections use + * the provided {@link BandwidthMeter} to determine which individual track should be used during + * playback. + * + * @param bandwidthMeter The {@link BandwidthMeter}. + */ + public DefaultTrackSelector(BandwidthMeter bandwidthMeter) { + this(new AdaptiveTrackSelection.Factory(bandwidthMeter)); } /** 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 86dc5cfedf..bb2a952b11 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 @@ -54,8 +54,8 @@ public final class CacheDataSource implements DataSource { FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}) public @interface Flags {} /** - * A flag indicating whether we will block reads if the cache key is locked. If this flag is - * set, then we will read from upstream if the cache key is locked. + * A flag indicating whether we will block reads if the cache key is locked. If unset then data is + * read from upstream if the cache key is locked, regardless of whether the data is cached. */ public static final int FLAG_BLOCK_ON_CACHE = 1 << 0; @@ -110,7 +110,23 @@ public final class CacheDataSource implements DataSource { /** * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for - * reading and writing the cache and with {@link #DEFAULT_MAX_CACHE_FILE_SIZE}. + * reading and writing the cache. + * + * @param cache The cache. + * @param upstream A {@link DataSource} for reading data not in the cache. + */ + public CacheDataSource(Cache cache, DataSource upstream) { + this(cache, upstream, 0, DEFAULT_MAX_CACHE_FILE_SIZE); + } + + /** + * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for + * reading and writing the cache. + * + * @param cache The cache. + * @param upstream A {@link DataSource} for reading data not in the cache. + * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} + * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0. */ public CacheDataSource(Cache cache, DataSource upstream, @Flags int flags) { this(cache, upstream, flags, DEFAULT_MAX_CACHE_FILE_SIZE); @@ -123,8 +139,8 @@ public final class CacheDataSource implements DataSource { * * @param cache The cache. * @param upstream A {@link DataSource} for reading data not in the cache. - * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link - * #FLAG_IGNORE_CACHE_ON_ERROR} or 0. + * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} + * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0. * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the cached data size * exceeds this value, then the data will be fragmented into multiple cache files. The * finer-grained this is the finer-grained the eviction policy can be. @@ -145,8 +161,8 @@ public final class CacheDataSource implements DataSource { * @param cacheReadDataSource A {@link DataSource} for reading data from the cache. * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is * accessed read-only. - * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link - * #FLAG_IGNORE_CACHE_ON_ERROR} or 0. + * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} + * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0. * @param eventListener An optional {@link EventListener} to receive events. */ public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java index b6fa3b4e2c..f0285da274 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java @@ -33,18 +33,26 @@ public final class CacheDataSourceFactory implements DataSource.Factory { private final int flags; private final EventListener eventListener; + /** + * @see CacheDataSource#CacheDataSource(Cache, DataSource) + */ + public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory) { + this(cache, upstreamFactory, 0); + } + /** * @see CacheDataSource#CacheDataSource(Cache, DataSource, int) */ - public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, int flags) { + public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, + @CacheDataSource.Flags int flags) { this(cache, upstreamFactory, flags, CacheDataSource.DEFAULT_MAX_CACHE_FILE_SIZE); } /** * @see CacheDataSource#CacheDataSource(Cache, DataSource, int, long) */ - public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, int flags, - long maxCacheFileSize) { + public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, + @CacheDataSource.Flags int flags, long maxCacheFileSize) { this(cache, upstreamFactory, new FileDataSourceFactory(), new CacheDataSinkFactory(cache, maxCacheFileSize), flags, null); } @@ -54,8 +62,8 @@ public final class CacheDataSourceFactory implements DataSource.Factory { * EventListener) */ public CacheDataSourceFactory(Cache cache, Factory upstreamFactory, - Factory cacheReadDataSourceFactory, - DataSink.Factory cacheWriteDataSinkFactory, int flags, EventListener eventListener) { + Factory cacheReadDataSourceFactory, DataSink.Factory cacheWriteDataSinkFactory, + @CacheDataSource.Flags int flags, EventListener eventListener) { this.cache = cache; this.upstreamFactory = upstreamFactory; this.cacheReadDataSourceFactory = cacheReadDataSourceFactory; From 5d459c8103a00735260a2f849c00f98a1d6e865d Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 5 May 2017 03:49:22 -0700 Subject: [PATCH 015/220] Expose no CC tracks if CLOSED-CAPTIONS=NONE is present Issue:#2743 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155182859 --- .../playlist/HlsMasterPlaylistParserTest.java | 14 ++++- .../exoplayer2/source/hls/HlsChunkSource.java | 3 +- .../exoplayer2/source/hls/HlsMediaChunk.java | 11 +++- .../hls/playlist/HlsMasterPlaylist.java | 62 ++++++++++++++++--- .../hls/playlist/HlsPlaylistParser.java | 13 +++- 5 files changed, 90 insertions(+), 13 deletions(-) diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index aa279f23f4..912dcb28b2 100644 --- a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.util.MimeTypes; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.Charset; +import java.util.Collections; import java.util.List; import junit.framework.TestCase; @@ -56,16 +57,22 @@ public class HlsMasterPlaylistParserTest extends TestCase { private static final String MASTER_PLAYLIST_WITH_CC = " #EXTM3U \n" + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n" - + "\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "http://example.com/low.m3u8\n"; + private static final String MASTER_PLAYLIST_WITHOUT_CC = " #EXTM3U \n" + + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128," + + "CLOSED-CAPTIONS=NONE\n" + + "http://example.com/low.m3u8\n"; + public void testParseMasterPlaylist() throws IOException{ HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST); List variants = masterPlaylist.variants; assertNotNull(variants); assertEquals(5, variants.size()); + assertNull(masterPlaylist.muxedCaptionFormats); assertEquals(1280000, variants.get(0).format.bitrate); assertNotNull(variants.get(0).format.codecs); @@ -117,6 +124,11 @@ public class HlsMasterPlaylistParserTest extends TestCase { assertEquals("es", closedCaptionFormat.language); } + public void testPlaylistWithoutClosedCaptions() throws IOException { + HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST_WITHOUT_CC); + assertEquals(Collections.emptyList(), playlist.muxedCaptionFormats); + } + private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) throws IOException { Uri playlistUri = Uri.parse(uri); 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 ea99dae345..49c4d04abc 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 @@ -111,7 +111,8 @@ import java.util.Locale; * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * same provider. - * @param muxedCaptionFormats List of muxed caption {@link Format}s. + * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption + * information is available in the master playlist. */ public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants, HlsDataSourceFactory dataSourceFactory, TimestampAdjusterProvider timestampAdjusterProvider, diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 6f516923f9..6997324f02 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -104,7 +105,8 @@ import java.util.concurrent.atomic.AtomicInteger; * @param dataSpec Defines the data to be loaded. * @param initDataSpec Defines the initialization data to be fed to new extractors. May be null. * @param hlsUrl The url of the playlist from which this chunk was obtained. - * @param muxedCaptionFormats List of muxed caption {@link Format}s. + * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption + * information is available in the master playlist. * @param trackSelectionReason See {@link #trackSelectionReason}. * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the chunk in microseconds. @@ -356,9 +358,12 @@ import java.util.concurrent.atomic.AtomicInteger; // This flag ensures the change of pid between streams does not affect the sample queues. @DefaultTsPayloadReaderFactory.Flags int esReaderFactoryFlags = DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM; - if (!muxedCaptionFormats.isEmpty()) { + List closedCaptionFormats = muxedCaptionFormats; + if (closedCaptionFormats != null) { // The playlist declares closed caption renditions, we should ignore descriptors. esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS; + } else { + closedCaptionFormats = Collections.emptyList(); } String codecs = trackFormat.codecs; if (!TextUtils.isEmpty(codecs)) { @@ -373,7 +378,7 @@ import java.util.concurrent.atomic.AtomicInteger; } } extractor = new TsExtractor(TsExtractor.MODE_HLS, timestampAdjuster, - new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, muxedCaptionFormats)); + new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, closedCaptionFormats)); } if (usingNewExtractor) { extractor.init(extractorOutput); 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 5a8c63f609..874c865049 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 @@ -30,15 +30,31 @@ public final class HlsMasterPlaylist extends HlsPlaylist { */ public static final class HlsUrl { + /** + * The http url from which the media playlist can be obtained. + */ public final String url; + /** + * Format information associated with the HLS url. + */ public final Format format; - public static HlsUrl createMediaPlaylistHlsUrl(String baseUri) { + /** + * Creates an HLS url from a given http url. + * + * @param url The url. + * @return An HLS url. + */ + public static HlsUrl createMediaPlaylistHlsUrl(String url) { Format format = Format.createContainerFormat("0", MimeTypes.APPLICATION_M3U8, null, null, Format.NO_VALUE, 0, null); - return new HlsUrl(baseUri, format); + return new HlsUrl(url, format); } + /** + * @param url See {@link #url}. + * @param format See {@link #format}. + */ public HlsUrl(String url, Format format) { this.url = url; this.format = format; @@ -46,13 +62,39 @@ public final class HlsMasterPlaylist extends HlsPlaylist { } + /** + * The list of variants declared by the playlist. + */ public final List variants; + /** + * The list of demuxed audios declared by the playlist. + */ public final List audios; + /** + * The list of subtitles declared by the playlist. + */ public final List subtitles; + /** + * 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; + /** + * 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 + * captions information. + */ public final List muxedCaptionFormats; + /** + * @param baseUri The base uri. Used to resolve relative paths. + * @param variants See {@link #variants}. + * @param audios See {@link #audios}. + * @param subtitles See {@link #subtitles}. + * @param muxedAudioFormat See {@link #muxedAudioFormat}. + * @param muxedCaptionFormats See {@link #muxedCaptionFormats}. + */ public HlsMasterPlaylist(String baseUri, List variants, List audios, List subtitles, Format muxedAudioFormat, List muxedCaptionFormats) { super(baseUri); @@ -60,14 +102,20 @@ public final class HlsMasterPlaylist extends HlsPlaylist { this.audios = Collections.unmodifiableList(audios); this.subtitles = Collections.unmodifiableList(subtitles); this.muxedAudioFormat = muxedAudioFormat; - this.muxedCaptionFormats = Collections.unmodifiableList(muxedCaptionFormats); + this.muxedCaptionFormats = muxedCaptionFormats != null + ? Collections.unmodifiableList(muxedCaptionFormats) : null; } - public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUri) { - List variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUri)); + /** + * Creates a playlist with a single variant. + * + * @param variantUrl The url of the single variant. + * @return A master playlist with a single variant for the provided url. + */ + public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUrl) { + List variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUrl)); List emptyList = Collections.emptyList(); - return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, - Collections.emptyList()); + return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, null); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 8e01dec6fe..664306baff 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Queue; @@ -70,6 +71,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser audios = new ArrayList<>(); ArrayList subtitles = new ArrayList<>(); Format muxedAudioFormat = null; - ArrayList muxedCaptionFormats = new ArrayList<>(); + List muxedCaptionFormats = null; + boolean noClosedCaptions = false; String line; while (iterator.hasNext()) { @@ -210,6 +214,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser(); + } muxedCaptionFormats.add(Format.createTextContainerFormat(id, null, mimeType, null, Format.NO_VALUE, selectionFlags, language, accessibilityChannel)); break; @@ -221,6 +228,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Fri, 5 May 2017 07:21:48 -0700 Subject: [PATCH 016/220] Don't set MAX_INPUT_SIZE to unnecessarily large values If the codec isn't adaptive, there's no need to accommodate the width/height/input-size of streams that don't have the same resolution as the current stream. This is because we'll always need to instantiate a new codec anyway. Issue: #2607 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155194458 --- .../video/MediaCodecVideoRenderer.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index ac4bb36035..473d61e3fa 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 @@ -408,11 +408,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive, Format oldFormat, Format newFormat) { - return areAdaptationCompatible(oldFormat, newFormat) + return areAdaptationCompatible(codecIsAdaptive, oldFormat, newFormat) && newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height - && newFormat.maxInputSize <= codecMaxValues.inputSize - && (codecIsAdaptive - || (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height)); + && newFormat.maxInputSize <= codecMaxValues.inputSize; } @Override @@ -664,7 +662,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } boolean haveUnknownDimensions = false; for (Format streamFormat : streamFormats) { - if (areAdaptationCompatible(format, streamFormat)) { + if (areAdaptationCompatible(codecInfo.adaptive, format, streamFormat)) { haveUnknownDimensions |= (streamFormat.width == Format.NO_VALUE || streamFormat.height == Format.NO_VALUE); maxWidth = Math.max(maxWidth, streamFormat.width); @@ -817,17 +815,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } /** - * Returns whether an adaptive codec with suitable {@link CodecMaxValues} will support adaptation - * between two {@link Format}s. + * Returns whether a codec with suitable {@link CodecMaxValues} will support adaptation between + * two {@link Format}s. * + * @param codecIsAdaptive Whether the codec supports seamless resolution switches. * @param first The first format. * @param second The second format. - * @return Whether an adaptive codec with suitable {@link CodecMaxValues} will support adaptation - * between two {@link Format}s. + * @return Whether the codec will support adaptation between the two {@link Format}s. */ - private static boolean areAdaptationCompatible(Format first, Format second) { + private static boolean areAdaptationCompatible(boolean codecIsAdaptive, Format first, + Format second) { return first.sampleMimeType.equals(second.sampleMimeType) - && getRotationDegrees(first) == getRotationDegrees(second); + && getRotationDegrees(first) == getRotationDegrees(second) + && (codecIsAdaptive || (first.width == second.width && first.height == second.height)); } private static float getPixelWidthHeightRatio(Format format) { From 4f9cf44986b0dd381f43cc1bfa1f84e6a62fdbc6 Mon Sep 17 00:00:00 2001 From: arakawa_yusuke Date: Mon, 8 May 2017 20:05:10 +0900 Subject: [PATCH 017/220] Remove fully-qualified name --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index ac4bb36035..73ee781b48 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 @@ -376,7 +376,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected void onOutputFormatChanged(MediaCodec codec, android.media.MediaFormat outputFormat) { + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT) && outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM) && outputFormat.containsKey(KEY_CROP_TOP); From 4b5c521a3354eb573087a14f0da927b2aed44bef Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Sun, 7 May 2017 08:18:25 -0700 Subject: [PATCH 018/220] Use native byte order for SimpleOutputBuffers The default byte order for ByteBuffers is big endian, but platform decoder output buffers use native byte order. AudioProcessors handle native byte order input/output. When using a software audio decoding extension the Sonic audio processor would receive big endian input but was outputting to a native byte order buffer, which could be little endian. This mismatch caused audio output to be distorted. After this change both platform decoder and extension decoder output buffers should be in native byte order. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155320973 --- .../google/android/exoplayer2/decoder/SimpleOutputBuffer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java index 309c7fd144..49c7dafbd6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.decoder; import java.nio.ByteBuffer; +import java.nio.ByteOrder; /** * Buffer for {@link SimpleDecoder} output. @@ -40,7 +41,7 @@ public class SimpleOutputBuffer extends OutputBuffer { public ByteBuffer init(long timeUs, int size) { this.timeUs = timeUs; if (data == null || data.capacity() < size) { - data = ByteBuffer.allocateDirect(size); + data = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()); } data.position(0); data.limit(size); From 631cce9171f37f98a71caa6921f5d589af4bbf4f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 8 May 2017 00:25:59 -0700 Subject: [PATCH 019/220] Fix interpolation for rate/pitch adjustment Based on https://github.com/waywardgeek/sonic/commit/7b441933. Issue: #2774 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155349817 --- .../java/com/google/android/exoplayer2/audio/Sonic.java | 6 +++--- 1 file changed, 3 insertions(+), 3 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 5d6f01b6e0..ef7877ae1e 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 @@ -374,8 +374,8 @@ import java.util.Arrays; } private short interpolate(short[] in, int inPos, int oldSampleRate, int newSampleRate) { - short left = in[inPos * numChannels]; - short right = in[inPos * numChannels + numChannels]; + short left = in[inPos]; + short right = in[inPos + numChannels]; int position = newRatePosition * oldSampleRate; int leftPosition = oldRatePosition * newSampleRate; int rightPosition = (oldRatePosition + 1) * newSampleRate; @@ -402,7 +402,7 @@ import java.util.Arrays; enlargeOutputBufferIfNeeded(1); for (int i = 0; i < numChannels; i++) { outputBuffer[numOutputSamples * numChannels + i] = - interpolate(pitchBuffer, position + i, oldSampleRate, newSampleRate); + interpolate(pitchBuffer, position * numChannels + i, oldSampleRate, newSampleRate); } newRatePosition++; numOutputSamples++; From df0d1b0f8a815a4a890690d714aee07952000a26 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 8 May 2017 09:19:27 -0700 Subject: [PATCH 020/220] Removed superfluous dots in russian translation of repeat modes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155386511 --- library/ui/src/main/res/values-ru/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/ui/src/main/res/values-ru/strings.xml b/library/ui/src/main/res/values-ru/strings.xml index bfff499a0a..1d179e028c 100644 --- a/library/ui/src/main/res/values-ru/strings.xml +++ b/library/ui/src/main/res/values-ru/strings.xml @@ -22,7 +22,7 @@ "Остановить" "Перемотать назад" "Перемотать вперед" - "Повторять все." - "Не повторять." - "Повторять один элемент." + "Повторять все" + "Не повторять" + "Повторять один элемент" From c70cd37c5a28e6b7fa2e9b87c032ff5a2b501ee4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 8 May 2017 09:19:56 -0700 Subject: [PATCH 021/220] Repeat mode UI Added repeat mode toggle buttons to UI. Current mode gets forwarded to Exoplayer instance, but without playback behaviour changes yet. Translations for button descriptions are also missing - this will be another CL. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155386549 --- .../android/exoplayer2/demo/EventLogger.java | 4 + .../google/android/exoplayer2/ExoPlayer.java | 10 +- .../exoplayer2/ui/PlaybackControlView.java | 171 +++++++++++++++++- .../exoplayer2/ui/SimpleExoPlayerView.java | 10 + .../exo_controls_repeat_all.xml | 23 +++ .../exo_controls_repeat_off.xml | 23 +++ .../exo_controls_repeat_one.xml | 23 +++ .../drawable-hdpi/exo_controls_repeat_all.png | Bin 0 -> 203 bytes .../drawable-hdpi/exo_controls_repeat_off.png | Bin 0 -> 223 bytes .../drawable-hdpi/exo_controls_repeat_one.png | Bin 0 -> 223 bytes .../drawable-ldpi/exo_controls_repeat_all.png | Bin 0 -> 142 bytes .../drawable-ldpi/exo_controls_repeat_off.png | Bin 0 -> 166 bytes .../drawable-ldpi/exo_controls_repeat_one.png | Bin 0 -> 160 bytes .../drawable-mdpi/exo_controls_repeat_all.png | Bin 0 -> 210 bytes .../drawable-mdpi/exo_controls_repeat_off.png | Bin 0 -> 227 bytes .../drawable-mdpi/exo_controls_repeat_one.png | Bin 0 -> 232 bytes .../exo_controls_repeat_all.png | Bin 0 -> 288 bytes .../exo_controls_repeat_off.png | Bin 0 -> 322 bytes .../exo_controls_repeat_one.png | Bin 0 -> 331 bytes .../exo_controls_repeat_all.png | Bin 0 -> 266 bytes .../exo_controls_repeat_off.png | Bin 0 -> 309 bytes .../exo_controls_repeat_one.png | Bin 0 -> 309 bytes .../res/layout/exo_playback_control_view.xml | 3 + library/ui/src/main/res/values/attrs.xml | 6 + library/ui/src/main/res/values/ids.xml | 1 + 25 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_all.xml create mode 100644 library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_off.xml create mode 100644 library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_one.xml create mode 100644 library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_all.png create mode 100644 library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_off.png create mode 100644 library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_one.png create mode 100644 library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_all.png create mode 100644 library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_off.png create mode 100644 library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_one.png create mode 100644 library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_all.png create mode 100644 library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_off.png create mode 100644 library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_one.png create mode 100644 library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_all.png create mode 100644 library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_off.png create mode 100644 library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_one.png create mode 100644 library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_all.png create mode 100644 library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_off.png create mode 100644 library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_one.png diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 0d77624a7b..686718f9e0 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -470,6 +470,10 @@ import java.util.Locale; switch (repeatMode) { case ExoPlayer.REPEAT_MODE_OFF: return "OFF"; + case ExoPlayer.REPEAT_MODE_ONE: + return "ONE"; + case ExoPlayer.REPEAT_MODE_ALL: + return "ALL"; default: return "?"; } 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 6327ebd9c2..8e16de5fca 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 @@ -268,12 +268,20 @@ public interface ExoPlayer { * Repeat modes for playback. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({REPEAT_MODE_OFF}) + @IntDef({REPEAT_MODE_OFF, REPEAT_MODE_ONE, REPEAT_MODE_ALL}) public @interface RepeatMode {} /** * Normal playback without repetition. */ int REPEAT_MODE_OFF = 0; + /** + * "Repeat One" mode to repeat the currently playing window infinitely. + */ + int REPEAT_MODE_ONE = 1; + /** + * "Repeat All" mode to repeat the entire timeline infinitely. + */ + int REPEAT_MODE_ALL = 2; /** * Register a listener to receive events from the player. The listener's methods will be called on diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index ae5b7c8b13..337cb47b1e 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -18,13 +18,17 @@ package com.google.android.exoplayer2.ui; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; +import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; import android.os.SystemClock; +import android.support.annotation.IntDef; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.widget.FrameLayout; +import android.widget.ImageView; import android.widget.TextView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -35,6 +39,8 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.Formatter; import java.util.Locale; @@ -70,6 +76,14 @@ import java.util.Locale; *

  • Default: {@link #DEFAULT_FAST_FORWARD_MS}
  • * * + *
  • {@code repeat_toggle_modes} - A flagged enumeration value specifying which repeat + * mode toggle options are enabled. Valid values are: {@code none}, {@code one}, + * {@code all}, or {@code one|all}. + *
      + *
    • Corresponding method: {@link #setRepeatToggleModes(int)}
    • + *
    • Default: {@link #DEFAULT_REPEAT_TOGGLE_MODES}
    • + *
    + *
  • *
  • {@code controller_layout_id} - Specifies the id of the layout to be inflated. See * below for more details. *
      @@ -117,6 +131,11 @@ import java.util.Locale; *
    • Type: {@link View}
    • *
    *
  • + *
  • {@code exo_repeat_toggle} - The repeat toggle button. + *
      + *
    • Type: {@link View}
    • + *
    + *
  • *
  • {@code exo_position} - Text view displaying the current playback position. *
      *
    • Type: {@link TextView}
    • @@ -189,6 +208,14 @@ public class PlaybackControlView extends FrameLayout { */ boolean dispatchSeekTo(ExoPlayer player, int windowIndex, long positionMs); + /** + * Dispatches a {@link ExoPlayer#setRepeatMode(int)} operation. + * + * @param player The player to which the operation should be dispatched. + * @param repeatMode The repeat mode. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchSetRepeatMode(ExoPlayer player, @ExoPlayer.RepeatMode int repeatMode); } /** @@ -209,11 +236,38 @@ public class PlaybackControlView extends FrameLayout { return true; } + @Override + public boolean dispatchSetRepeatMode(ExoPlayer player, @ExoPlayer.RepeatMode int repeatMode) { + player.setRepeatMode(repeatMode); + return true; + } + }; + /** + * Set of repeat toggle modes. Can be combined using bit-wise operations. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = {REPEAT_TOGGLE_MODE_NONE, REPEAT_TOGGLE_MODE_ONE, + REPEAT_TOGGLE_MODE_ALL}) + public @interface RepeatToggleModes {} + /** + * All repeat mode buttons disabled. + */ + public static final int REPEAT_TOGGLE_MODE_NONE = 0; + /** + * "Repeat One" button enabled. + */ + public static final int REPEAT_TOGGLE_MODE_ONE = 1; + /** + * "Repeat All" button enabled. + */ + public static final int REPEAT_TOGGLE_MODE_ALL = 2; + public static final int DEFAULT_FAST_FORWARD_MS = 15000; public static final int DEFAULT_REWIND_MS = 5000; public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000; + public static final @RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = REPEAT_TOGGLE_MODE_NONE; /** * The maximum number of windows that can be shown in a multi-window time bar. @@ -229,6 +283,7 @@ public class PlaybackControlView extends FrameLayout { private final View pauseButton; private final View fastForwardButton; private final View rewindButton; + private final ImageView repeatToggleButton; private final TextView durationView; private final TextView positionView; private final TimeBar timeBar; @@ -237,6 +292,13 @@ public class PlaybackControlView extends FrameLayout { private final Timeline.Period period; private final Timeline.Window window; + private final Drawable repeatOffButtonDrawable; + private final Drawable repeatOneButtonDrawable; + private final Drawable repeatAllButtonDrawable; + private final String repeatOffButtonContentDescription; + private final String repeatOneButtonContentDescription; + private final String repeatAllButtonContentDescription; + private ExoPlayer player; private ControlDispatcher controlDispatcher; private VisibilityListener visibilityListener; @@ -248,6 +310,7 @@ public class PlaybackControlView extends FrameLayout { private int rewindMs; private int fastForwardMs; private int showTimeoutMs; + private @RepeatToggleModes int repeatToggleModes; private long hideAtMs; private long[] adBreakTimesMs; @@ -280,6 +343,7 @@ public class PlaybackControlView extends FrameLayout { rewindMs = DEFAULT_REWIND_MS; fastForwardMs = DEFAULT_FAST_FORWARD_MS; showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; + repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlaybackControlView, 0, 0); @@ -290,6 +354,7 @@ public class PlaybackControlView extends FrameLayout { showTimeoutMs = a.getInt(R.styleable.PlaybackControlView_show_timeout, showTimeoutMs); controllerLayoutId = a.getResourceId(R.styleable.PlaybackControlView_controller_layout_id, controllerLayoutId); + repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes); } finally { a.recycle(); } @@ -335,6 +400,26 @@ public class PlaybackControlView extends FrameLayout { if (fastForwardButton != null) { fastForwardButton.setOnClickListener(componentListener); } + repeatToggleButton = (ImageView) findViewById(R.id.exo_repeat_toggle); + if (repeatToggleButton != null) { + repeatToggleButton.setOnClickListener(componentListener); + } + Resources resources = context.getResources(); + repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off); + repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one); + repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_all); + repeatOffButtonContentDescription = resources.getString( + R.string.exo_controls_repeat_off_description); + repeatOneButtonContentDescription = resources.getString( + R.string.exo_controls_repeat_one_description); + repeatAllButtonContentDescription = resources.getString( + R.string.exo_controls_repeat_all_description); + } + + @SuppressWarnings("ResourceType") + private static @RepeatToggleModes int getRepeatToggleModes(TypedArray a, + @RepeatToggleModes int repeatToggleModes) { + return a.getInt(R.styleable.PlaybackControlView_repeat_toggle_modes, repeatToggleModes); } /** @@ -440,6 +525,37 @@ public class PlaybackControlView extends FrameLayout { this.showTimeoutMs = showTimeoutMs; } + /** + * Returns which repeat toggle modes are enabled. + * + * @return The currently enabled {@link RepeatToggleModes}. + */ + public @RepeatToggleModes int getRepeatToggleModes() { + return repeatToggleModes; + } + + /** + * Sets which repeat toggle modes are enabled. + * + * @param repeatToggleModes A set of {@link RepeatToggleModes}. + */ + public void setRepeatToggleModes(@RepeatToggleModes int repeatToggleModes) { + this.repeatToggleModes = repeatToggleModes; + if (player != null) { + @ExoPlayer.RepeatMode int currentMode = player.getRepeatMode(); + if (repeatToggleModes == REPEAT_TOGGLE_MODE_NONE + && currentMode != ExoPlayer.REPEAT_MODE_OFF) { + controlDispatcher.dispatchSetRepeatMode(player, ExoPlayer.REPEAT_MODE_OFF); + } else if (repeatToggleModes == REPEAT_TOGGLE_MODE_ONE + && currentMode == ExoPlayer.REPEAT_MODE_ALL) { + controlDispatcher.dispatchSetRepeatMode(player, ExoPlayer.REPEAT_MODE_ONE); + } else if (repeatToggleModes == REPEAT_TOGGLE_MODE_ALL + && currentMode == ExoPlayer.REPEAT_MODE_ONE) { + controlDispatcher.dispatchSetRepeatMode(player, ExoPlayer.REPEAT_MODE_ALL); + } + } + } + /** * Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will * be automatically hidden after this duration of time has elapsed without user input. @@ -494,6 +610,7 @@ public class PlaybackControlView extends FrameLayout { private void updateAll() { updatePlayPauseButton(); updateNavigation(); + updateRepeatModeButton(); updateProgress(); } @@ -546,6 +663,31 @@ public class PlaybackControlView extends FrameLayout { } } + private void updateRepeatModeButton() { + if (!isVisible() || !isAttachedToWindow) { + return; + } + if (repeatToggleModes == REPEAT_TOGGLE_MODE_NONE) { + repeatToggleButton.setVisibility(View.GONE); + return; + } + switch (player.getRepeatMode()) { + case ExoPlayer.REPEAT_MODE_OFF: + repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); + repeatToggleButton.setContentDescription(repeatOffButtonContentDescription); + break; + case ExoPlayer.REPEAT_MODE_ONE: + repeatToggleButton.setImageDrawable(repeatOneButtonDrawable); + repeatToggleButton.setContentDescription(repeatOneButtonContentDescription); + break; + case ExoPlayer.REPEAT_MODE_ALL: + repeatToggleButton.setImageDrawable(repeatAllButtonDrawable); + repeatToggleButton.setContentDescription(repeatAllButtonContentDescription); + break; + } + repeatToggleButton.setVisibility(View.VISIBLE); + } + private void updateTimeBarMode() { if (player == null) { return; @@ -705,6 +847,30 @@ public class PlaybackControlView extends FrameLayout { } } + private @ExoPlayer.RepeatMode int getNextRepeatMode() { + @ExoPlayer.RepeatMode int currentMode = player.getRepeatMode(); + for (int offset = 1; offset <= 2; offset++) { + @ExoPlayer.RepeatMode int proposedMode = (currentMode + offset) % 3; + if (isRepeatModeEnabled(proposedMode)) { + return proposedMode; + } + } + return currentMode; + } + + private boolean isRepeatModeEnabled(@ExoPlayer.RepeatMode int repeatMode) { + switch (repeatMode) { + case ExoPlayer.REPEAT_MODE_OFF: + return true; + case ExoPlayer.REPEAT_MODE_ONE: + return (repeatToggleModes & REPEAT_TOGGLE_MODE_ONE) != 0; + case ExoPlayer.REPEAT_MODE_ALL: + return (repeatToggleModes & REPEAT_TOGGLE_MODE_ALL) != 0; + default: + return false; + } + } + private void rewind() { if (rewindMs <= 0) { return; @@ -908,7 +1074,8 @@ public class PlaybackControlView extends FrameLayout { @Override public void onRepeatModeChanged(int repeatMode) { - // Do nothing. + updateRepeatModeButton(); + updateNavigation(); } @Override @@ -959,6 +1126,8 @@ public class PlaybackControlView extends FrameLayout { controlDispatcher.dispatchSetPlayWhenReady(player, true); } else if (pauseButton == view) { controlDispatcher.dispatchSetPlayWhenReady(player, false); + } else if (repeatToggleButton == view) { + controlDispatcher.dispatchSetRepeatMode(player, getNextRepeatMode()); } } hideAfterTimeout(); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 5219778109..5cbfb638a5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -591,6 +591,16 @@ public final class SimpleExoPlayerView extends FrameLayout { controller.setFastForwardIncrementMs(fastForwardMs); } + /** + * Sets which repeat toggle modes are enabled. + * + * @param repeatToggleModes A set of {@link PlaybackControlView.RepeatToggleModes}. + */ + public void setRepeatToggleModes(@PlaybackControlView.RepeatToggleModes int repeatToggleModes) { + Assertions.checkState(controller != null); + controller.setRepeatToggleModes(repeatToggleModes); + } + /** * Sets whether the time bar should show all windows, as opposed to just the current one. * diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_all.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_all.xml new file mode 100644 index 0000000000..dad37fa1f0 --- /dev/null +++ b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_all.xml @@ -0,0 +1,23 @@ + + + + diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_off.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_off.xml new file mode 100644 index 0000000000..132eae0d76 --- /dev/null +++ b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_off.xml @@ -0,0 +1,23 @@ + + + + diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_one.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_one.xml new file mode 100644 index 0000000000..d51010566a --- /dev/null +++ b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_one.xml @@ -0,0 +1,23 @@ + + + + diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_all.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_all.png new file mode 100644 index 0000000000000000000000000000000000000000..2824e7847c490a8f3a79c10e95fc7b4425480f36 GIT binary patch literal 203 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtNuDl_Ar*{ouWaOP2oN}Y;r0)= z#3uo}mj2+-IV$|bbY}gPDDO>W4p%xZWHDyBoap4(+kB#Yt++!rdrT0+<9_C61^mZm zGxUfvN$9f}#Bn6-5js$z+`yHwmeX%hgNinbft_ay!!d0}sET{634h#I9-q$?lf}TH v{D3jZr*VOzdeVtbiFAcyk{v%?*KUS5PN+l&~*%+u6{1-oD!MOr+|pbped50J@B5bw4tnbyC=om# zcylV3qw)u*3mkg{OeCvY7YLR#DK7Y5$}GWfm*E)WXY1>%tvf4Z9{f^h{`QA$-I)m- zS`L#oIlw)4SD0@UU*J=HIknYaFbY%753t*g-%`{bk!>F;9PggiXAccv$DaX8uQTB*zuG9MaAm=l9y85}Sb4q9e E07KDJ@&Et; literal 0 HcmV?d00001 diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_all.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_all.png new file mode 100644 index 0000000000000000000000000000000000000000..5c91a47519092e1a22738c198ba5e9525fcaa82f GIT binary patch literal 142 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`Gjp`I>|Ar*{ouQ0MX1ah!mEV~mF z^{Ga_MPVM}EY>^D8td#8av4My-d5Z*)hr-eVcOY3#us3;W6xx!m5dHd91i{33=?d& mFrH#z6AE+iJn%>VI(zlSX$1zyy_N$lVDNPHb6Mw<&;$Smi7d&^EJwwS%-c!vhAr z7X|Xy8>)84FrKStx^$T_k+~t@-Gj?Jn0k)4_BC!{ko_nhva8|S{x^3FN*XRc{JCW< P&|(HpS3j3^P67Fi*Ar*{ouWS@-FyLWvobHmY z)hHChrK;z=hK12fBL2k#^G(NIab4uia9z++5njUl*~}ns+A(|fPZhEU+M0Que@t)q z^RMv6e@+KUw=Ikuul_GExyPiiBAhWzeZqu`qG`et-k*NO@^JlW$q61;lz>(;c)I$z JtaD0e0swHLIa>e# literal 0 HcmV?d00001 diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_all.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_all.png new file mode 100644 index 0000000000000000000000000000000000000000..97f7e1cc7590310a3b6aa322584e6e1ec8b6bab9 GIT binary patch literal 210 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UGd*1#Ln;{WTv;f}6v*Rpkwb+w zhtq@0;Yi>yeZ|z8ovaEPt(W8kJ50|Mozp0DJ1;8O`JXFYTw)5h!^{Si2#=eblM+5; zpJEMT^*J`(?nn*u5<4Z^@`icV4(DYPIA!1M*>&9_Z^y?saZc@*-%SkFxWu4gz`(`x zz_{J%fAf`MN!B%L8x+-?%Q`Q(9LqDg*x>%8dP;bLGtkX;(TjdEt_l1NbR>hPtDnm{ Hr-UW|Cn-tw literal 0 HcmV?d00001 diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_off.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_off.png new file mode 100644 index 0000000000000000000000000000000000000000..6a0232170238bb285a24d6b34e43247005cc2169 GIT binary patch literal 227 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4Ut2|vCLn;`rTw!EmHWX=jI7MiM z>*5B7298|;Cpfuw{xAH;w6MQ#D*Ii@4Kw&>dsu|;sA}~2$khCZMa5C&WLTTZiJoTN zxCNRECe?q@d@k(qNPX5%ri5ow36H;ioqINh*`$pz`NBiT<(qvMe4ck8>cxN7g7oy~ zVkJ8w9OO51wmxCdV9!gM^qKX*8FttG>?*d`MIDM6+E%38lxR}eW4_+9!E?!Zjr9lG bL>L%2bOKr?dByevUCrR>>gTe~DWM4fJ|9<5 literal 0 HcmV?d00001 diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_one.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_one.png new file mode 100644 index 0000000000000000000000000000000000000000..59bac337054e11eac123964a46de9c937d15b3b9 GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4U>pfi@Ln;`rUKJEzHWX-m=&;eQ zP-~ul;@Wot%S+o6YPd>Q1>d>5(XZZnF3UULW!ihUi&V&`luasGQ_#o6+RyaB(LpH1 z$B)%BAfng3aan`c&6&Rg#2Y%jMZSh6tjlibeO{t>rj++WgX9^{cImn2zOh=ZXZ)f1 ze(=loKuWATqb^&I%hD4kH93`bRr)fV gZaHed!TBjKXZ&Rgrb(UmfKF%dboFyt=akR{0P5IV-v9sr literal 0 HcmV?d00001 diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_all.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_all.png new file mode 100644 index 0000000000000000000000000000000000000000..2baaedecbff58a5b4141266f7ed74804fe1d034b GIT binary patch literal 288 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_7U=bkQ(Ar*{oZx|LbI|{H~Y+HPR zLBztCf!UyilQmh9X9{x?0~hbYxf&atdt>)6x%H3XnQHmLO}A|e=Ur|%?8Ut062n>( z2W{R9K&DfJ;K6R&TcPeO{Qq}>q#CBO>g?bz__`xGzWMWhhO>W+V|t$^nuUwz#f|iXZvrTvq{=LSAXBVvZQPO`YXDabgVfxm|&nkH=lo~U6B6# zdFi`9vJGGD8*~;Xy6vdyDQlEmC~2;2lETRw>Et}^!UReRm#EJ(%Qe`=D7jfWz+g9{$_)k; z2}TtQ5QB-4d(sx^E*A&4pP$d=F>v}Fc*Sh6pMQhm`y2fe)-!n8w4K?_07r#!>(_)ngB)2D?Z)X{-ahQv$FPb-yZ%AR*Wh$7#I_s zr4aSDbU2^Y3#5Z==u%2ZJ&pAdYrFn_{&hMD_$mGhZh z?lE@k;TEVAUoiJM18-Gb{klshm>op(8Ko-OE@(c=Juvsm?FP?Zyazrr=goR_?A1xj zxcUNySxMlR4NrfDZ{qo+=kO%9;lyr+iQgGt z?fKHQpFd^V^V9qO@Eyov+QKPwfMYh-E>%^x6P)}43=KjC4Z;uvbmgZ1@0UMbxBflz X#E2BlHybV;1_hF*tDnm{r-UW|-I#^I literal 0 HcmV?d00001 diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_all.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_all.png new file mode 100644 index 0000000000000000000000000000000000000000..d7207ebc0d296d8d33a9de048883837b982a7e7d GIT binary patch literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeG3?%1&o4*=J@dWsUxB}__Fko@OdIgZrQWE4B z%#i=TU(UMI1;}6N>Eakt!Fc!NLP2H)0hf!(GaQ2$mM&P)o~6NbxN!l?tA$-Rx_{^y z)@-%*pSM=Pq{+{KV+%5M=I1&HSM8wE4VL2#Gi+HC1aq304Uj2k1^edj!GDD_%u$XM)e8a6A;w&@ZStI$t(%W3R^9Kj%M(5^>bP0 Hl+XkK`_Wv{ literal 0 HcmV?d00001 diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_off.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_off.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6253ead693a726bdcd46b1421318d52bf8f325 GIT binary patch literal 309 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeE3?v1%WpM*3p#Yx{S0Mc#71#x>wE~K=l?3?( zGbs6L^O-t)+`SVha>moeF{Fa=?b(BzEe--M7qfgE)RSMBvf65t-N3SQ)9Z;lHp{cI7`5{L(N5a5$8o9 dSO4aA`^e8F3nN!ATnF?QgQu&X%Q~loCIHE)abExc literal 0 HcmV?d00001 diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_one.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_one.png new file mode 100644 index 0000000000000000000000000000000000000000..d577f4ebcd6cf5eb5a30319b407bc9901e585e40 GIT binary patch literal 309 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeE3?v1%WpM*3p#Yx{S0Mc#71#x>wE~K=l?3?( zGvxp8H&}n8-Sjk2Hj zW)=|^O%ARm5f)ZQuBKg#zpAWF+3rdQBwyHWVajqhIw1Lh?1g5ATkLY|Ilo;MG%^HQ z6f*>l{4-?ZZ8Bg%;15svm_Sk@e>7%jNT|99TY10ur^a-xCBw-?ah8B@hnkD9I duKvyK_K}}U7DldKxDMzq22WQ%mvv4FO#ooxa~J>s literal 0 HcmV?d00001 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 1d6267e7f0..407329890d 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 @@ -34,6 +34,9 @@ + + diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index d8340c21cd..2cb28709b6 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -34,6 +34,11 @@ + + + + + @@ -57,6 +62,7 @@ + diff --git a/library/ui/src/main/res/values/ids.xml b/library/ui/src/main/res/values/ids.xml index 61db83825e..815487a54e 100644 --- a/library/ui/src/main/res/values/ids.xml +++ b/library/ui/src/main/res/values/ids.xml @@ -27,6 +27,7 @@ + From 812068a2087c2d5c1b7b84b39881aadd4a33555a Mon Sep 17 00:00:00 2001 From: olly Date: Sat, 7 Jan 2017 11:59:20 +0000 Subject: [PATCH 022/220] Avoid process death if OOM occurs on a loading thread This is most commonly caused by malformed media, where the media indicates that something we need to make an allocation for is *really huge*. Failing playback is appropriate for this case; killing the process is not. Issue: #2780 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155408062 --- .../google/android/exoplayer2/upstream/Loader.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index bca90ddc5c..1bdebf7c17 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 @@ -33,11 +33,11 @@ import java.util.concurrent.ExecutorService; public final class Loader implements LoaderErrorThrower { /** - * Thrown when an unexpected exception is encountered during loading. + * Thrown when an unexpected exception or error is encountered during loading. */ public static final class UnexpectedLoaderException extends IOException { - public UnexpectedLoaderException(Exception cause) { + public UnexpectedLoaderException(Throwable cause) { super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause); } @@ -316,6 +316,14 @@ public final class Loader implements LoaderErrorThrower { if (!released) { obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget(); } + } catch (OutOfMemoryError e) { + // This can occur if a stream is malformed in a way that causes an extractor to think it + // needs to allocate a large amount of memory. We don't want the process to die in this + // case, but we do want the playback to fail. + Log.e(TAG, "OutOfMemory error loading stream", e); + if (!released) { + obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget(); + } } catch (Error e) { // We'd hope that the platform would kill the process if an Error is thrown here, but the // executor may catch the error (b/20616433). Throw it here, but also pass and throw it from From b2997180362d9a1cec5647259eaa7cdca844cb72 Mon Sep 17 00:00:00 2001 From: tasnimsunny Date: Mon, 8 May 2017 12:46:09 -0700 Subject: [PATCH 023/220] Make removal of non-existent cache span a no-op ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155413733 --- .../google/android/exoplayer2/upstream/cache/SimpleCache.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 14f006c850..bbff7dc4a2 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 @@ -286,7 +286,9 @@ public final class SimpleCache implements Cache { private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) throws CacheException { CachedContent cachedContent = index.get(span.key); - Assertions.checkState(cachedContent.removeSpan(span)); + if (cachedContent == null || !cachedContent.removeSpan(span)) { + return; + } totalSpace -= span.length; if (removeEmptyCachedContent && cachedContent.isEmpty()) { index.removeEmpty(cachedContent.key); From 86ac913df61b6fc79d8ed5a905c775ed10c832ff Mon Sep 17 00:00:00 2001 From: arakawa_yusuke Date: Tue, 9 May 2017 21:20:57 +0900 Subject: [PATCH 024/220] Fix unused variable --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b8b1314504..d5f4cdf6c0 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 @@ -1259,7 +1259,7 @@ import java.io.IOException; // We are already buffering the maximum number of periods ahead. return; } - newLoadingPeriodIndex = timeline.getNextPeriodIndex(loadingPeriodHolder.index, period, window, + newLoadingPeriodIndex = timeline.getNextPeriodIndex(loadingPeriodIndex, period, window, repeatMode); } From 1f43fb19985964566c0613205e39dbc0f4d5bb6d Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 9 May 2017 02:20:43 -0700 Subject: [PATCH 025/220] Introduce CryptoData parameter object This will allow supporting more encryption schemes. Including some that require more encryption data, like the encryption pattern. Issue:#1661 Issue:#1989 Issue:#2089 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155481889 --- .../subsample_encrypted_altref.webm.0.dump | 1 + .../subsample_encrypted_noaltref.webm.0.dump | 1 + .../java/com/google/android/exoplayer2/C.java | 4 +- .../extractor/DefaultTrackOutput.java | 27 +++++------ .../extractor/DummyTrackOutput.java | 2 +- .../exoplayer2/extractor/TrackOutput.java | 46 ++++++++++++++++++- .../extractor/mkv/MatroskaExtractor.java | 15 +++--- .../extractor/mp4/FragmentedMp4Extractor.java | 37 +++++++++++---- .../source/chunk/ChunkExtractorWrapper.java | 4 +- .../exoplayer2/testutil/FakeTrackOutput.java | 32 ++++++------- 10 files changed, 114 insertions(+), 55 deletions(-) diff --git a/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump index 1932ab78f7..f533e14c3f 100644 --- a/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump +++ b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump @@ -30,5 +30,6 @@ track 1: time = 0 flags = 1073741824 data = length 39, hash B7FE77F4 + crypto mode = 1 encryption key = length 16, hash 4CE944CF tracksEnded = true diff --git a/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump index 8751c99b20..d84c549dea 100644 --- a/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump +++ b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump @@ -30,5 +30,6 @@ track 1: time = 0 flags = 1073741824 data = length 24, hash E58668B1 + crypto mode = 1 encryption key = length 16, hash 4CE944CF tracksEnded = true 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 35a69df39e..a9eeea6be3 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 @@ -83,12 +83,12 @@ public final class C { public static final String UTF16_NAME = "UTF-16"; /** - * * The name of the serif font family. + * The name of the serif font family. */ public static final String SERIF_NAME = "serif"; /** - * * The name of the sans-serif font family. + * The name of the sans-serif font family. */ public static final String SANS_SERIF_NAME = "sans-serif"; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java index 1c9a148226..c879d8e695 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java @@ -366,8 +366,9 @@ public final class DefaultTrackOutput implements TrackOutput { } // Populate the cryptoInfo. + CryptoData cryptoData = extrasHolder.cryptoData; buffer.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes, - extrasHolder.encryptionKeyId, buffer.cryptoInfo.iv, C.CRYPTO_MODE_AES_CTR); + cryptoData.encryptionKey, buffer.cryptoInfo.iv, cryptoData.cryptoMode); // Adjust the offset and size to take into account the bytes read. int bytesRead = (int) (offset - extrasHolder.offset); @@ -516,7 +517,7 @@ public final class DefaultTrackOutput implements TrackOutput { @Override public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { + CryptoData cryptoData) { if (pendingFormatAdjustment) { format(lastUnadjustedFormat); } @@ -533,7 +534,7 @@ public final class DefaultTrackOutput implements TrackOutput { } timeUs += sampleOffsetUs; long absoluteOffset = totalBytesWritten - size - offset; - infoQueue.commitSample(timeUs, flags, absoluteOffset, size, encryptionKey); + infoQueue.commitSample(timeUs, flags, absoluteOffset, size, cryptoData); } finally { endWriteOperation(); } @@ -606,7 +607,7 @@ public final class DefaultTrackOutput implements TrackOutput { private int[] sizes; private int[] flags; private long[] timesUs; - private byte[][] encryptionKeys; + private CryptoData[] cryptoDatas; private Format[] formats; private int queueSize; @@ -628,7 +629,7 @@ public final class DefaultTrackOutput implements TrackOutput { timesUs = new long[capacity]; flags = new int[capacity]; sizes = new int[capacity]; - encryptionKeys = new byte[capacity][]; + cryptoDatas = new CryptoData[capacity]; formats = new Format[capacity]; largestDequeuedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE; @@ -792,7 +793,7 @@ public final class DefaultTrackOutput implements TrackOutput { buffer.setFlags(flags[relativeReadIndex]); extrasHolder.size = sizes[relativeReadIndex]; extrasHolder.offset = offsets[relativeReadIndex]; - extrasHolder.encryptionKeyId = encryptionKeys[relativeReadIndex]; + extrasHolder.cryptoData = cryptoDatas[relativeReadIndex]; largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, buffer.timeUs); queueSize--; @@ -892,7 +893,7 @@ public final class DefaultTrackOutput implements TrackOutput { } public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset, - int size, byte[] encryptionKey) { + int size, CryptoData cryptoData) { if (upstreamKeyframeRequired) { if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) { return; @@ -905,7 +906,7 @@ public final class DefaultTrackOutput implements TrackOutput { offsets[relativeWriteIndex] = offset; sizes[relativeWriteIndex] = size; flags[relativeWriteIndex] = sampleFlags; - encryptionKeys[relativeWriteIndex] = encryptionKey; + cryptoDatas[relativeWriteIndex] = cryptoData; formats[relativeWriteIndex] = upstreamFormat; sourceIds[relativeWriteIndex] = upstreamSourceId; // Increment the write index. @@ -918,14 +919,14 @@ public final class DefaultTrackOutput implements TrackOutput { long[] newTimesUs = new long[newCapacity]; int[] newFlags = new int[newCapacity]; int[] newSizes = new int[newCapacity]; - byte[][] newEncryptionKeys = new byte[newCapacity][]; + CryptoData[] newCryptoDatas = new CryptoData[newCapacity]; Format[] newFormats = new Format[newCapacity]; int beforeWrap = capacity - relativeReadIndex; System.arraycopy(offsets, relativeReadIndex, newOffsets, 0, beforeWrap); System.arraycopy(timesUs, relativeReadIndex, newTimesUs, 0, beforeWrap); System.arraycopy(flags, relativeReadIndex, newFlags, 0, beforeWrap); System.arraycopy(sizes, relativeReadIndex, newSizes, 0, beforeWrap); - System.arraycopy(encryptionKeys, relativeReadIndex, newEncryptionKeys, 0, beforeWrap); + System.arraycopy(cryptoDatas, relativeReadIndex, newCryptoDatas, 0, beforeWrap); System.arraycopy(formats, relativeReadIndex, newFormats, 0, beforeWrap); System.arraycopy(sourceIds, relativeReadIndex, newSourceIds, 0, beforeWrap); int afterWrap = relativeReadIndex; @@ -933,14 +934,14 @@ public final class DefaultTrackOutput implements TrackOutput { System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap); System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap); System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap); - System.arraycopy(encryptionKeys, 0, newEncryptionKeys, beforeWrap, afterWrap); + System.arraycopy(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap); System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap); System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap); offsets = newOffsets; timesUs = newTimesUs; flags = newFlags; sizes = newSizes; - encryptionKeys = newEncryptionKeys; + cryptoDatas = newCryptoDatas; formats = newFormats; sourceIds = newSourceIds; relativeReadIndex = 0; @@ -990,7 +991,7 @@ public final class DefaultTrackOutput implements TrackOutput { public int size; public long offset; public long nextOffset; - public byte[] encryptionKeyId; + public CryptoData cryptoData; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java index 61f97887be..c023b0de95 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java @@ -51,7 +51,7 @@ public final class DummyTrackOutput implements TrackOutput { @Override public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { + CryptoData cryptoData) { // Do nothing. } 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 c4dee4b6a7..2054854796 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 @@ -20,12 +20,54 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.EOFException; import java.io.IOException; +import java.util.Arrays; /** * Receives track level data extracted by an {@link Extractor}. */ public interface TrackOutput { + /** + * Holds data required to decrypt a sample. + */ + final class CryptoData { + + /** + * The encryption mode used for the sample. + */ + @C.CryptoMode public final int cryptoMode; + + /** + * The encryption key associated with the sample. Its contents must not be modified. + */ + public final byte[] encryptionKey; + + public CryptoData(@C.CryptoMode int cryptoMode, byte[] encryptionKey) { + this.cryptoMode = cryptoMode; + this.encryptionKey = encryptionKey; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + CryptoData other = (CryptoData) obj; + return cryptoMode == other.cryptoMode && Arrays.equals(encryptionKey, other.encryptionKey); + } + + @Override + public int hashCode() { + int result = cryptoMode; + result = 31 * result + Arrays.hashCode(encryptionKey); + return result; + } + + } + /** * Called when the {@link Format} of the track has been extracted from the stream. * @@ -70,9 +112,9 @@ public interface TrackOutput { * {@link #sampleData(ExtractorInput, int, boolean)} or * {@link #sampleData(ParsableByteArray, int)} since the last byte belonging to the sample * whose metadata is being passed. - * @param encryptionKey The encryption key associated with the sample. May be null. + * @param encryptionData The encryption data required to decrypt the sample. May be null. */ void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey); + CryptoData encryptionData); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 8f3abf4688..227cbd6f0c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -580,11 +580,11 @@ public final class MatroskaExtractor implements Extractor { break; case ID_CONTENT_ENCODING: if (currentTrack.hasContentEncryption) { - if (currentTrack.encryptionKeyId == null) { + if (currentTrack.cryptoData == null) { throw new ParserException("Encrypted Track found but ContentEncKeyID was not found"); } - currentTrack.drmInitData = new DrmInitData( - new SchemeData(C.UUID_NIL, MimeTypes.VIDEO_WEBM, currentTrack.encryptionKeyId)); + currentTrack.drmInitData = new DrmInitData(new SchemeData(C.UUID_NIL, + MimeTypes.VIDEO_WEBM, currentTrack.cryptoData.encryptionKey)); } break; case ID_CONTENT_ENCODINGS: @@ -888,8 +888,9 @@ public final class MatroskaExtractor implements Extractor { input.readFully(currentTrack.sampleStrippedBytes, 0, contentSize); break; case ID_CONTENT_ENCRYPTION_KEY_ID: - currentTrack.encryptionKeyId = new byte[contentSize]; - input.readFully(currentTrack.encryptionKeyId, 0, contentSize); + byte[] encryptionKey = new byte[contentSize]; + input.readFully(encryptionKey, 0, contentSize); + currentTrack.cryptoData = new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, encryptionKey); break; case ID_SIMPLE_BLOCK: case ID_BLOCK: @@ -1033,7 +1034,7 @@ public final class MatroskaExtractor implements Extractor { if (CODEC_ID_SUBRIP.equals(track.codecId)) { writeSubripSample(track); } - track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.encryptionKeyId); + track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData); sampleRead = true; resetSample(); } @@ -1470,7 +1471,7 @@ public final class MatroskaExtractor implements Extractor { public int defaultSampleDurationNs; public boolean hasContentEncryption; public byte[] sampleStrippedBytes; - public byte[] encryptionKeyId; + public TrackOutput.CryptoData cryptoData; public byte[] codecPrivate; public DrmInitData drmInitData; 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 a228a9b775..fe1d4b04af 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 @@ -1122,19 +1122,30 @@ public final class FragmentedMp4Extractor implements Extractor { } long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L; - @C.BufferFlags int sampleFlags = (fragment.definesEncryptionData ? C.BUFFER_FLAG_ENCRYPTED : 0) - | (fragment.sampleIsSyncFrameTable[sampleIndex] ? C.BUFFER_FLAG_KEY_FRAME : 0); - int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex; - byte[] encryptionKey = null; - if (fragment.definesEncryptionData) { - encryptionKey = fragment.trackEncryptionBox != null - ? fragment.trackEncryptionBox.keyId - : track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex].keyId; - } if (timestampAdjuster != null) { sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); } - output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey); + + @C.BufferFlags int sampleFlags = (fragment.definesEncryptionData ? C.BUFFER_FLAG_ENCRYPTED : 0) + | (fragment.sampleIsSyncFrameTable[sampleIndex] ? C.BUFFER_FLAG_KEY_FRAME : 0); + + // Encryption data. + TrackOutput.CryptoData cryptoData = null; + TrackEncryptionBox encryptionBox = null; + if (fragment.definesEncryptionData) { + encryptionBox = fragment.trackEncryptionBox != null + ? fragment.trackEncryptionBox + : track.sampleDescriptionEncryptionBoxes[fragment.header.sampleDescriptionIndex]; + if (encryptionBox != currentTrackBundle.cachedEncryptionBox) { + cryptoData = new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, encryptionBox.keyId); + } else { + cryptoData = currentTrackBundle.cachedCryptoData; + } + } + currentTrackBundle.cachedCryptoData = cryptoData; + currentTrackBundle.cachedEncryptionBox = encryptionBox; + + output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, cryptoData); while (!pendingMetadataSampleInfos.isEmpty()) { MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst(); @@ -1288,6 +1299,10 @@ public final class FragmentedMp4Extractor implements Extractor { public int currentSampleInTrackRun; public int currentTrackRunIndex; + // Auxiliary references. + public TrackOutput.CryptoData cachedCryptoData; + public TrackEncryptionBox cachedEncryptionBox; + public TrackBundle(TrackOutput output) { fragment = new TrackFragment(); this.output = output; @@ -1305,6 +1320,8 @@ public final class FragmentedMp4Extractor implements Extractor { currentSampleIndex = 0; currentTrackRunIndex = 0; currentSampleInTrackRun = 0; + cachedCryptoData = null; + cachedEncryptionBox = null; } public void updateDrmInitData(DrmInitData drmInitData) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index 501f4998cf..07d1cce8cb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -186,8 +186,8 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { @Override public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { - trackOutput.sampleMetadata(timeUs, flags, size, offset, encryptionKey); + CryptoData cryptoData) { + trackOutput.sampleMetadata(timeUs, flags, size, offset, cryptoData); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java index b399d79e8d..b14e6f60ef 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java @@ -36,7 +36,7 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { private final ArrayList sampleFlags; private final ArrayList sampleStartOffsets; private final ArrayList sampleEndOffsets; - private final ArrayList sampleEncryptionKeys; + private final ArrayList cryptoDatas; private byte[] sampleData; public Format format; @@ -47,7 +47,7 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { sampleFlags = new ArrayList<>(); sampleStartOffsets = new ArrayList<>(); sampleEndOffsets = new ArrayList<>(); - sampleEncryptionKeys = new ArrayList<>(); + cryptoDatas = new ArrayList<>(); } public void clear() { @@ -56,7 +56,7 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { sampleFlags.clear(); sampleStartOffsets.clear(); sampleEndOffsets.clear(); - sampleEncryptionKeys.clear(); + cryptoDatas.clear(); } @Override @@ -89,29 +89,24 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { @Override public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { + CryptoData cryptoData) { sampleTimesUs.add(timeUs); sampleFlags.add(flags); sampleStartOffsets.add(sampleData.length - offset - size); sampleEndOffsets.add(sampleData.length - offset); - sampleEncryptionKeys.add(encryptionKey); + cryptoDatas.add(cryptoData); } public void assertSampleCount(int count) { Assert.assertEquals(count, sampleTimesUs.size()); } - public void assertSample(int index, byte[] data, long timeUs, int flags, byte[] encryptionKey) { + public void assertSample(int index, byte[] data, long timeUs, int flags, CryptoData cryptoData) { byte[] actualData = getSampleData(index); MoreAsserts.assertEquals(data, actualData); Assert.assertEquals(timeUs, (long) sampleTimesUs.get(index)); Assert.assertEquals(flags, (int) sampleFlags.get(index)); - byte[] sampleEncryptionKey = sampleEncryptionKeys.get(index); - if (encryptionKey == null) { - Assert.assertEquals(null, sampleEncryptionKey); - } else { - MoreAsserts.assertEquals(encryptionKey, sampleEncryptionKey); - } + Assert.assertEquals(cryptoData, cryptoDatas.get(index)); } public byte[] getSampleData(int index) { @@ -128,10 +123,10 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { Assert.assertEquals(expected.sampleFlags.get(i), sampleFlags.get(i)); Assert.assertEquals(expected.sampleStartOffsets.get(i), sampleStartOffsets.get(i)); Assert.assertEquals(expected.sampleEndOffsets.get(i), sampleEndOffsets.get(i)); - if (expected.sampleEncryptionKeys.get(i) == null) { - Assert.assertNull(sampleEncryptionKeys.get(i)); + if (expected.cryptoDatas.get(i) == null) { + Assert.assertNull(cryptoDatas.get(i)); } else { - MoreAsserts.assertEquals(expected.sampleEncryptionKeys.get(i), sampleEncryptionKeys.get(i)); + Assert.assertEquals(expected.cryptoDatas.get(i), cryptoDatas.get(i)); } } } @@ -172,9 +167,10 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { .add("time", sampleTimesUs.get(i)) .add("flags", sampleFlags.get(i)) .add("data", getSampleData(i)); - byte[] key = sampleEncryptionKeys.get(i); - if (key != null) { - dumper.add("encryption key", key); + CryptoData cryptoData = cryptoDatas.get(i); + if (cryptoData != null) { + dumper.add("crypto mode", cryptoData.cryptoMode); + dumper.add("encryption key", cryptoData.encryptionKey); } dumper.endBlock(); } From 002dd72e702929a33d8940cfbaa758459e0067c6 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 9 May 2017 03:14:58 -0700 Subject: [PATCH 026/220] Propagate playlist loading error if it prevents playback This imitates DashMediaSource's behavior. Issue:#2623 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155485738 --- .../exoplayer2/source/hls/HlsChunkSource.java | 7 +++++++ .../exoplayer2/source/hls/HlsMediaSource.java | 2 +- .../hls/playlist/HlsPlaylistTracker.java | 19 +++++++++++++++---- 3 files changed, 23 insertions(+), 5 deletions(-) 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 49c4d04abc..795e2f0eaa 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 @@ -92,6 +92,7 @@ import java.util.Locale; private boolean isTimestampMaster; private byte[] scratchSpace; private IOException fatalError; + private HlsUrl expectedPlaylistUrl; private Uri encryptionKeyUri; private byte[] encryptionKey; @@ -143,6 +144,9 @@ import java.util.Locale; if (fatalError != null) { throw fatalError; } + if (expectedPlaylistUrl != null) { + playlistTracker.maybeThrowPlaylistRefreshError(expectedPlaylistUrl); + } } /** @@ -195,6 +199,7 @@ import java.util.Locale; public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChunkHolder out) { int oldVariantIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); + expectedPlaylistUrl = null; // Use start time of the previous chunk rather than its end time because switching format will // require downloading overlapping segments. long bufferedDurationUs = previous == null ? 0 @@ -208,6 +213,7 @@ import java.util.Locale; HlsUrl selectedUrl = variants[selectedVariantIndex]; if (!playlistTracker.isSnapshotValid(selectedUrl)) { out.playlist = selectedUrl; + expectedPlaylistUrl = selectedUrl; // Retry when playlist is refreshed. return; } @@ -247,6 +253,7 @@ import java.util.Locale; out.endOfStream = true; } else /* Live */ { out.playlist = selectedUrl; + expectedPlaylistUrl = selectedUrl; } return; } 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 3cd9f19522..1bfb8371a0 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 @@ -84,7 +84,7 @@ public final class HlsMediaSource implements MediaSource, @Override public void maybeThrowSourceInfoRefreshError() throws IOException { - playlistTracker.maybeThrowPlaylistRefreshError(); + playlistTracker.maybeThrowPrimaryPlaylistRefreshError(); } @Override diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index 02a8e3f098..62b77a0575 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -200,18 +200,29 @@ public final class HlsPlaylistTracker implements Loader.Callback Date: Tue, 9 May 2017 03:20:40 -0700 Subject: [PATCH 027/220] Make MODE_SINGLE_PMT the default mode Even though this is not strictly spec compliant, this will make exoplayer behave like it used to before multiple program support. Developers who want to take advantage of the multiple program support are probably less than the ones who only want their stream to "just work". This is particularly useful for streams obtained after a filtering component, like a tv tuner. Issue:#2757 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155486122 --- .../extractor/ts/TsExtractorTest.java | 4 ++-- .../extractor/DefaultExtractorsFactory.java | 21 ++++++++++++++++++- .../exoplayer2/extractor/ts/TsExtractor.java | 21 ++++++++++++++----- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index 7bf722cd8f..efd653b8d9 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -75,7 +75,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { public void testCustomPesReader() throws Exception { CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false); - TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0), + TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0), factory); FakeExtractorInput input = new FakeExtractorInput.Builder() .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts")) @@ -100,7 +100,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { public void testCustomInitialSectionReader() throws Exception { CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true); - TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0), + TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0), factory); FakeExtractorInput input = new FakeExtractorInput.Builder() .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts")) 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 022ca1277d..c47a91b176 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 @@ -26,7 +26,9 @@ import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; import com.google.android.exoplayer2.extractor.ts.PsExtractor; import com.google.android.exoplayer2.extractor.ts.TsExtractor; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader; import com.google.android.exoplayer2.extractor.wav.WavExtractor; +import com.google.android.exoplayer2.util.TimestampAdjuster; import java.lang.reflect.Constructor; /** @@ -67,8 +69,13 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { private @MatroskaExtractor.Flags int matroskaFlags; private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags; private @Mp3Extractor.Flags int mp3Flags; + private @TsExtractor.Mode int tsMode; private @DefaultTsPayloadReaderFactory.Flags int tsFlags; + public DefaultExtractorsFactory() { + tsMode = TsExtractor.MODE_SINGLE_PMT; + } + /** * Sets flags for {@link MatroskaExtractor} instances created by the factory. * @@ -107,6 +114,18 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { return this; } + /** + * Sets the mode for {@link TsExtractor} instances created by the factory. + * + * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory). + * @param mode The mode to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setTsExtractorMode(@TsExtractor.Mode int mode) { + tsMode = mode; + return this; + } + /** * Sets flags for {@link DefaultTsPayloadReaderFactory}s used by {@link TsExtractor} instances * created by the factory. @@ -130,7 +149,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { extractors[3] = new Mp3Extractor(mp3Flags); extractors[4] = new AdtsExtractor(); extractors[5] = new Ac3Extractor(); - extractors[6] = new TsExtractor(tsFlags); + extractors[6] = new TsExtractor(tsMode, tsFlags); extractors[7] = new FlvExtractor(); extractors[8] = new OggExtractor(); extractors[9] = new PsExtractor(); 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 df6efb722c..71b8375bd8 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 @@ -65,13 +65,13 @@ public final class TsExtractor implements Extractor { * Modes for the extractor. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({MODE_NORMAL, MODE_SINGLE_PMT, MODE_HLS}) + @IntDef({MODE_MULTI_PMT, MODE_SINGLE_PMT, MODE_HLS}) public @interface Mode {} /** * Behave as defined in ISO/IEC 13818-1. */ - public static final int MODE_NORMAL = 0; + public static final int MODE_MULTI_PMT = 0; /** * Assume only one PMT will be contained in the stream, even if more are declared by the PAT. */ @@ -132,12 +132,23 @@ public final class TsExtractor implements Extractor { * {@code FLAG_*} values that control the behavior of the payload readers. */ public TsExtractor(@Flags int defaultTsPayloadReaderFlags) { - this(MODE_NORMAL, new TimestampAdjuster(0), - new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags)); + this(MODE_SINGLE_PMT, defaultTsPayloadReaderFlags); } /** - * @param mode Mode for the extractor. One of {@link #MODE_NORMAL}, {@link #MODE_SINGLE_PMT} + * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} + * and {@link #MODE_HLS}. + * @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory} + * {@code FLAG_*} values that control the behavior of the payload readers. + */ + public TsExtractor(@Mode int mode, @Flags int defaultTsPayloadReaderFlags) { + this(mode, new TimestampAdjuster(0), + new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags)); + } + + + /** + * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} * and {@link #MODE_HLS}. * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. * @param payloadReaderFactory Factory for injecting a custom set of payload readers. From 8a210becefce8eaa5c101a6e075ace13b990370e Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 9 May 2017 08:53:29 -0700 Subject: [PATCH 028/220] Add repeat mode support to Timeline implementations. (Relating to GitHub Issue #2577) The Timeline base class provides the default implementation. Timeline wrappers (e.g. ClippingTimeline, ConcatatedTimeline) forward all requests to the respective inner timelines. Some like ConcatenatedTimeline add their own additional logic to bridge between the child timelines. In addition, ConcatenatedTimeline and LoopingTimeline now have a common abstract base class as they share most of their code. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155509269 --- .../google/android/exoplayer2/Timeline.java | 22 ++- .../source/AbstractConcatenatedTimeline.java | 145 ++++++++++++++++++ .../source/ClippingMediaSource.java | 11 ++ .../source/ConcatenatingMediaSource.java | 98 ++++++------ .../exoplayer2/source/LoopingMediaSource.java | 68 ++++---- 5 files changed, 255 insertions(+), 89 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java 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 8dc30b0905..1a33985c68 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 @@ -145,7 +145,16 @@ public abstract class Timeline { * @return The index of the next window, or {@link C#INDEX_UNSET} if this is the last window. */ public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { - return windowIndex == getWindowCount() - 1 ? C.INDEX_UNSET : windowIndex + 1; + switch (repeatMode) { + case ExoPlayer.REPEAT_MODE_OFF: + return windowIndex == getWindowCount() - 1 ? C.INDEX_UNSET : windowIndex + 1; + case ExoPlayer.REPEAT_MODE_ONE: + return windowIndex; + case ExoPlayer.REPEAT_MODE_ALL: + return windowIndex == getWindowCount() - 1 ? 0 : windowIndex + 1; + default: + throw new IllegalStateException(); + } } /** @@ -157,7 +166,16 @@ public abstract class Timeline { * @return The index of the previous window, or {@link C#INDEX_UNSET} if this is the first window. */ public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { - return windowIndex == 0 ? C.INDEX_UNSET : windowIndex - 1; + switch (repeatMode) { + case ExoPlayer.REPEAT_MODE_OFF: + return windowIndex == 0 ? C.INDEX_UNSET : windowIndex - 1; + case ExoPlayer.REPEAT_MODE_ONE: + return windowIndex; + case ExoPlayer.REPEAT_MODE_ALL: + return windowIndex == 0 ? getWindowCount() - 1 : windowIndex - 1; + default: + throw new IllegalStateException(); + } } /** 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 new file mode 100644 index 0000000000..37672703d7 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -0,0 +1,145 @@ +/* + * 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.source; + +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Timeline; + +/** + * Abstract base class for the concatenation of one or more {@link Timeline}s. + */ +/* package */ abstract class AbstractConcatenatedTimeline extends Timeline { + + @Override + public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + int childIndex = getChildIndexForWindow(windowIndex); + int firstWindowIndexInChild = getFirstWindowIndexInChild(childIndex); + int nextWindowIndexInChild = getChild(childIndex).getNextWindowIndex( + windowIndex - firstWindowIndexInChild, + repeatMode == ExoPlayer.REPEAT_MODE_ALL ? ExoPlayer.REPEAT_MODE_OFF : repeatMode); + if (nextWindowIndexInChild == C.INDEX_UNSET) { + if (childIndex < getChildCount() - 1) { + childIndex++; + } else if (repeatMode == ExoPlayer.REPEAT_MODE_ALL) { + childIndex = 0; + } else { + return C.INDEX_UNSET; + } + firstWindowIndexInChild = getFirstWindowIndexInChild(childIndex); + nextWindowIndexInChild = 0; + } + return firstWindowIndexInChild + nextWindowIndexInChild; + } + + @Override + public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + int childIndex = getChildIndexForWindow(windowIndex); + int firstWindowIndexInChild = getFirstWindowIndexInChild(childIndex); + int previousWindowIndexInChild = getChild(childIndex).getPreviousWindowIndex( + windowIndex - firstWindowIndexInChild, + repeatMode == ExoPlayer.REPEAT_MODE_ALL ? ExoPlayer.REPEAT_MODE_OFF : repeatMode); + if (previousWindowIndexInChild == C.INDEX_UNSET) { + if (childIndex > 0) { + childIndex--; + } else if (repeatMode == ExoPlayer.REPEAT_MODE_ALL) { + childIndex = getChildCount() - 1; + } else { + return C.INDEX_UNSET; + } + firstWindowIndexInChild = getFirstWindowIndexInChild(childIndex); + previousWindowIndexInChild = getChild(childIndex).getWindowCount() - 1; + } + return firstWindowIndexInChild + previousWindowIndexInChild; + } + + @Override + public final Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + int childIndex = getChildIndexForWindow(windowIndex); + int firstWindowIndexInChild = getFirstWindowIndexInChild(childIndex); + int firstPeriodIndexInChild = getFirstPeriodIndexInChild(childIndex); + getChild(childIndex).getWindow(windowIndex - firstWindowIndexInChild, window, setIds, + defaultPositionProjectionUs); + window.firstPeriodIndex += firstPeriodIndexInChild; + window.lastPeriodIndex += firstPeriodIndexInChild; + return window; + } + + @Override + public final Period getPeriod(int periodIndex, Period period, boolean setIds) { + int childIndex = getChildIndexForPeriod(periodIndex); + int firstWindowIndexInChild = getFirstWindowIndexInChild(childIndex); + int firstPeriodIndexInChild = getFirstPeriodIndexInChild(childIndex); + getChild(childIndex).getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds); + period.windowIndex += firstWindowIndexInChild; + if (setIds) { + period.uid = Pair.create(childIndex, period.uid); + } + return period; + } + + @Override + public final int getIndexOfPeriod(Object uid) { + if (!(uid instanceof Pair)) { + return C.INDEX_UNSET; + } + Pair childIndexAndPeriodId = (Pair) uid; + if (!(childIndexAndPeriodId.first instanceof Integer)) { + return C.INDEX_UNSET; + } + int childIndex = (Integer) childIndexAndPeriodId.first; + Object periodId = childIndexAndPeriodId.second; + if (childIndex < 0 || childIndex >= getChildCount()) { + return C.INDEX_UNSET; + } + int periodIndexInChild = getChild(childIndex).getIndexOfPeriod(periodId); + return periodIndexInChild == C.INDEX_UNSET ? C.INDEX_UNSET + : getFirstPeriodIndexInChild(childIndex) + periodIndexInChild; + } + + /** + * Returns the number of concatenated child timelines. + */ + protected abstract int getChildCount(); + + /** + * Returns a child timeline by index. + */ + protected abstract Timeline getChild(int childIndex); + + /** + * Returns the index of the child timeline to which the period with the given index belongs. + */ + protected abstract int getChildIndexForPeriod(int periodIndex); + + /** + * Returns the first period index belonging to the child timeline with the given index. + */ + protected abstract int getFirstPeriodIndexInChild(int childIndex); + + /** + * Returns the index of the child timeline to which the window with the given index belongs. + */ + protected abstract int getChildIndexForWindow(int windowIndex); + + /** + * Returns the first window index belonging to the child timeline with the given index. + */ + protected abstract int getFirstWindowIndexInChild(int childIndex); + +} 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 be15a07726..c61dea9553 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayer.RepeatMode; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; @@ -142,6 +143,16 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste return 1; } + @Override + public int getNextWindowIndex(int windowIndex, @RepeatMode int repeatMode) { + return timeline.getNextWindowIndex(windowIndex, repeatMode); + } + + @Override + public int getPreviousWindowIndex(int windowIndex, @RepeatMode int repeatMode) { + return timeline.getPreviousWindowIndex(windowIndex, repeatMode); + } + @Override public Window getWindow(int windowIndex, Window window, boolean setIds, long defaultPositionProjectionUs) { 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 9fc499f251..2299e757d7 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 @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer2.source; -import android.util.Pair; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; @@ -38,6 +36,7 @@ public final class ConcatenatingMediaSource implements MediaSource { private final Object[] manifests; private final Map sourceIndexByMediaPeriod; private final boolean[] duplicateFlags; + private final boolean isRepeatOneAtomic; private Listener listener; private ConcatenatedTimeline timeline; @@ -47,7 +46,19 @@ public final class ConcatenatingMediaSource implements MediaSource { * {@link MediaSource} instance to be present more than once in the array. */ public ConcatenatingMediaSource(MediaSource... mediaSources) { + this(false, mediaSources); + } + + /** + * @param isRepeatOneAtomic Whether the concatenated media source shall be treated as atomic + * (i.e., repeated in its entirety) when repeat mode is set to + * {@code ExoPlayer.REPEAT_MODE_ONE}. + * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same + * {@link MediaSource} instance to be present more than once in the array. + */ + public ConcatenatingMediaSource(boolean isRepeatOneAtomic, MediaSource... mediaSources) { this.mediaSources = mediaSources; + this.isRepeatOneAtomic = isRepeatOneAtomic; timelines = new Timeline[mediaSources.length]; manifests = new Object[mediaSources.length]; sourceIndexByMediaPeriod = new HashMap<>(); @@ -81,8 +92,8 @@ public final class ConcatenatingMediaSource implements MediaSource { @Override public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - int sourceIndex = timeline.getSourceIndexForPeriod(index); - int periodIndexInSource = index - timeline.getFirstPeriodIndexInSource(sourceIndex); + int sourceIndex = timeline.getChildIndexForPeriod(index); + int periodIndexInSource = index - timeline.getFirstPeriodIndexInChild(sourceIndex); MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIndexInSource, allocator, positionUs); sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex); @@ -123,7 +134,7 @@ public final class ConcatenatingMediaSource implements MediaSource { return; } } - timeline = new ConcatenatedTimeline(timelines.clone()); + timeline = new ConcatenatedTimeline(timelines.clone(), isRepeatOneAtomic); listener.onSourceInfoRefreshed(timeline, manifests.clone()); } @@ -144,13 +155,14 @@ public final class ConcatenatingMediaSource implements MediaSource { /** * A {@link Timeline} that is the concatenation of one or more {@link Timeline}s. */ - private static final class ConcatenatedTimeline extends Timeline { + private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline { private final Timeline[] timelines; private final int[] sourcePeriodOffsets; private final int[] sourceWindowOffsets; + private final boolean isRepeatOneAtomic; - public ConcatenatedTimeline(Timeline[] timelines) { + public ConcatenatedTimeline(Timeline[] timelines, boolean isRepeatOneAtomic) { int[] sourcePeriodOffsets = new int[timelines.length]; int[] sourceWindowOffsets = new int[timelines.length]; long periodCount = 0; @@ -167,6 +179,7 @@ public final class ConcatenatingMediaSource implements MediaSource { this.timelines = timelines; this.sourcePeriodOffsets = sourcePeriodOffsets; this.sourceWindowOffsets = sourceWindowOffsets; + this.isRepeatOneAtomic = isRepeatOneAtomic; } @Override @@ -174,70 +187,55 @@ public final class ConcatenatingMediaSource implements MediaSource { return sourceWindowOffsets[sourceWindowOffsets.length - 1]; } - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - int sourceIndex = getSourceIndexForWindow(windowIndex); - int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex); - int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex); - timelines[sourceIndex].getWindow(windowIndex - firstWindowIndexInSource, window, setIds, - defaultPositionProjectionUs); - window.firstPeriodIndex += firstPeriodIndexInSource; - window.lastPeriodIndex += firstPeriodIndexInSource; - return window; - } - @Override public int getPeriodCount() { return sourcePeriodOffsets[sourcePeriodOffsets.length - 1]; } @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - int sourceIndex = getSourceIndexForPeriod(periodIndex); - int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex); - int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex); - timelines[sourceIndex].getPeriod(periodIndex - firstPeriodIndexInSource, period, setIds); - period.windowIndex += firstWindowIndexInSource; - if (setIds) { - period.uid = Pair.create(sourceIndex, period.uid); + public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + if (isRepeatOneAtomic && repeatMode == ExoPlayer.REPEAT_MODE_ONE) { + repeatMode = ExoPlayer.REPEAT_MODE_ALL; } - return period; + return super.getNextWindowIndex(windowIndex, repeatMode); } @Override - public int getIndexOfPeriod(Object uid) { - if (!(uid instanceof Pair)) { - return C.INDEX_UNSET; + public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + if (isRepeatOneAtomic && repeatMode == ExoPlayer.REPEAT_MODE_ONE) { + repeatMode = ExoPlayer.REPEAT_MODE_ALL; } - Pair sourceIndexAndPeriodId = (Pair) uid; - if (!(sourceIndexAndPeriodId.first instanceof Integer)) { - return C.INDEX_UNSET; - } - int sourceIndex = (Integer) sourceIndexAndPeriodId.first; - Object periodId = sourceIndexAndPeriodId.second; - if (sourceIndex < 0 || sourceIndex >= timelines.length) { - return C.INDEX_UNSET; - } - int periodIndexInSource = timelines[sourceIndex].getIndexOfPeriod(periodId); - return periodIndexInSource == C.INDEX_UNSET ? C.INDEX_UNSET - : getFirstPeriodIndexInSource(sourceIndex) + periodIndexInSource; + return super.getPreviousWindowIndex(windowIndex, repeatMode); } - private int getSourceIndexForPeriod(int periodIndex) { + @Override + protected int getChildCount() { + return timelines.length; + } + + @Override + protected Timeline getChild(int childIndex) { + return timelines[childIndex]; + } + + @Override + protected int getChildIndexForPeriod(int periodIndex) { return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex, true, false) + 1; } - private int getFirstPeriodIndexInSource(int sourceIndex) { - return sourceIndex == 0 ? 0 : sourcePeriodOffsets[sourceIndex - 1]; + @Override + protected int getFirstPeriodIndexInChild(int childIndex) { + return childIndex == 0 ? 0 : sourcePeriodOffsets[childIndex - 1]; } - private int getSourceIndexForWindow(int windowIndex) { + @Override + protected int getChildIndexForWindow(int windowIndex) { return Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1; } - private int getFirstWindowIndexInSource(int sourceIndex) { - return sourceIndex == 0 ? 0 : sourceWindowOffsets[sourceIndex - 1]; + @Override + protected int getFirstWindowIndexInChild(int childIndex) { + return childIndex == 0 ? 0 : sourceWindowOffsets[childIndex - 1]; } } 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 0c872f199c..0e1e7d9033 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 @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.source; -import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; @@ -91,7 +90,7 @@ public final class LoopingMediaSource implements MediaSource { childSource.releaseSource(); } - private static final class LoopingTimeline extends Timeline { + private static final class LoopingTimeline extends AbstractConcatenatedTimeline { private final Timeline childTimeline; private final int childPeriodCount; @@ -112,47 +111,40 @@ public final class LoopingMediaSource implements MediaSource { return childWindowCount * loopCount; } - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - childTimeline.getWindow(windowIndex % childWindowCount, window, setIds, - defaultPositionProjectionUs); - int periodIndexOffset = (windowIndex / childWindowCount) * childPeriodCount; - window.firstPeriodIndex += periodIndexOffset; - window.lastPeriodIndex += periodIndexOffset; - return window; - } - @Override public int getPeriodCount() { return childPeriodCount * loopCount; } @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - childTimeline.getPeriod(periodIndex % childPeriodCount, period, setIds); - int loopCount = (periodIndex / childPeriodCount); - period.windowIndex += loopCount * childWindowCount; - if (setIds) { - period.uid = Pair.create(loopCount, period.uid); - } - return period; + protected Timeline getChild(int childIndex) { + return childTimeline; } @Override - public int getIndexOfPeriod(Object uid) { - if (!(uid instanceof Pair)) { - return C.INDEX_UNSET; - } - Pair loopCountAndChildUid = (Pair) uid; - if (!(loopCountAndChildUid.first instanceof Integer)) { - return C.INDEX_UNSET; - } - int loopCount = (Integer) loopCountAndChildUid.first; - int periodIndexOffset = loopCount * childPeriodCount; - return childTimeline.getIndexOfPeriod(loopCountAndChildUid.second) + periodIndexOffset; + protected int getChildCount() { + return loopCount; } + @Override + protected int getChildIndexForPeriod(int periodIndex) { + return periodIndex / childPeriodCount; + } + + @Override + protected int getFirstPeriodIndexInChild(int childIndex) { + return childIndex * childPeriodCount; + } + + @Override + protected int getChildIndexForWindow(int windowIndex) { + return windowIndex / childWindowCount; + } + + @Override + protected int getFirstWindowIndexInChild(int childIndex) { + return childIndex * childWindowCount; + } } private static final class InfinitelyLoopingTimeline extends Timeline { @@ -169,14 +161,16 @@ public final class LoopingMediaSource implements MediaSource { } @Override - public int getNextWindowIndex(int currentWindowIndex, @ExoPlayer.RepeatMode int repeatMode) { - return currentWindowIndex < getWindowCount() - 1 ? currentWindowIndex + 1 : 0; + public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + int childNextWindowIndex = childTimeline.getNextWindowIndex(windowIndex, repeatMode); + return childNextWindowIndex == C.INDEX_UNSET ? 0 : childNextWindowIndex; } @Override - public int getPreviousWindowIndex(int currentWindowIndex, - @ExoPlayer.RepeatMode int repeatMode) { - return currentWindowIndex > 0 ? currentWindowIndex - 1 : getWindowCount() - 1; + public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + int childPreviousWindowIndex = childTimeline.getPreviousWindowIndex(windowIndex, repeatMode); + return childPreviousWindowIndex == C.INDEX_UNSET ? getWindowCount() - 1 + : childPreviousWindowIndex; } @Override From 02c51ee01cf7d82983f89053f441543468e9a737 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 10 May 2017 00:52:33 -0700 Subject: [PATCH 029/220] Update period holders in ExoPlayerImplInternal when repeat mode changes. (Relating to GitHub issue #2577) Changing the repeat mode during playback may require to discard or rebuffer certain periods because the requested order of playback changed. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155600910 --- .../exoplayer2/ExoPlayerImplInternal.java | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) 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 b8b1314504..d6fa17c171 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 @@ -422,8 +422,49 @@ import java.io.IOException; } } - private void setRepeatModeInternal(@ExoPlayer.RepeatMode int repeatMode) { + private void setRepeatModeInternal(@ExoPlayer.RepeatMode int repeatMode) + throws ExoPlaybackException { this.repeatMode = repeatMode; + // Check if all existing period holders match the new period order. + MediaPeriodHolder lastValidPeriodHolder = playingPeriodHolder != null + ? playingPeriodHolder : loadingPeriodHolder; + if (lastValidPeriodHolder == null) { + return; + } + boolean seenReadingPeriodHolder = lastValidPeriodHolder == readingPeriodHolder; + boolean seenLoadingPeriodHolder = lastValidPeriodHolder == loadingPeriodHolder; + int nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.index, period, window, + repeatMode); + while (lastValidPeriodHolder.next != null && nextPeriodIndex != C.INDEX_UNSET + && lastValidPeriodHolder.next.index == nextPeriodIndex) { + lastValidPeriodHolder = lastValidPeriodHolder.next; + seenReadingPeriodHolder |= lastValidPeriodHolder == readingPeriodHolder; + seenLoadingPeriodHolder |= lastValidPeriodHolder == loadingPeriodHolder; + nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.index, period, window, + repeatMode); + } + // Release all period holder beyond the last one matching the new period order. + if (lastValidPeriodHolder.next != null) { + releasePeriodHoldersFrom(lastValidPeriodHolder.next); + lastValidPeriodHolder.next = null; + } + // Update isLast flag. + lastValidPeriodHolder.isLast = isLastPeriod(lastValidPeriodHolder.index); + // Handle cases where loadingPeriodHolder or readingPeriodHolder have been removed. + if (!seenLoadingPeriodHolder) { + loadingPeriodHolder = lastValidPeriodHolder; + } + if (!seenReadingPeriodHolder) { + // Renderers may have read from a period that's been removed. Seek back to the current + // position of the playing period to make sure none of the removed period is played. + int playingPeriodIndex = playingPeriodHolder.index; + long newPositionUs = seekToPeriodPosition(playingPeriodIndex, playbackInfo.positionUs); + playbackInfo = new PlaybackInfo(playingPeriodIndex, newPositionUs); + } + // Restart buffering if playback has ended and repetition is enabled. + if (state == ExoPlayer.STATE_ENDED && repeatMode != ExoPlayer.REPEAT_MODE_OFF) { + setState(ExoPlayer.STATE_BUFFERING); + } } private void startRenderers() throws ExoPlaybackException { From a6220b8be36410e971086b3204118816fd37463f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 10 May 2017 07:30:52 -0700 Subject: [PATCH 030/220] Handle control layouts with no repeat button ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155625893 --- .../com/google/android/exoplayer2/ui/PlaybackControlView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 337cb47b1e..d0f8c33b58 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -664,7 +664,7 @@ public class PlaybackControlView extends FrameLayout { } private void updateRepeatModeButton() { - if (!isVisible() || !isAttachedToWindow) { + if (!isVisible() || !isAttachedToWindow || repeatToggleButton == null) { return; } if (repeatToggleModes == REPEAT_TOGGLE_MODE_NONE) { From c0d16ea2cb523d28477385ef6d6b619c21f77d1e Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 10 May 2017 09:18:01 -0700 Subject: [PATCH 031/220] Possible NullPointerException in ExoPlayerImplInternal.setRepeatModeInternal When readingPeriodHolder and playingPeriodHolder are both null, a NullPointerException is thrown when trying to reassign readingPeriodHolder. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155635846 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d6fa17c171..45ff639d24 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 @@ -454,7 +454,7 @@ import java.io.IOException; if (!seenLoadingPeriodHolder) { loadingPeriodHolder = lastValidPeriodHolder; } - if (!seenReadingPeriodHolder) { + if (!seenReadingPeriodHolder && playingPeriodHolder != null) { // Renderers may have read from a period that's been removed. Seek back to the current // position of the playing period to make sure none of the removed period is played. int playingPeriodIndex = playingPeriodHolder.index; From 496306192340f39c67f6e9095cd2d0015794d2f5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 10 May 2017 19:36:06 -0700 Subject: [PATCH 032/220] Fix wrap_content handling in DefaultTimeBar Issue: #2788 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155705318 --- .../google/android/exoplayer2/ui/DefaultTimeBar.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 12f31f5da1..06ecb1aa69 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 @@ -343,16 +343,18 @@ public class DefaultTimeBar extends View implements TimeBar { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int measureWidth = MeasureSpec.getSize(widthMeasureSpec); - int measureHeight = MeasureSpec.getSize(heightMeasureSpec); - setMeasuredDimension(measureWidth, measureHeight); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int height = heightMode == MeasureSpec.UNSPECIFIED ? touchTargetHeight + : heightMode == MeasureSpec.EXACTLY ? heightSize : Math.min(touchTargetHeight, heightSize); + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), height); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { int width = right - left; int height = bottom - top; - int barY = height - touchTargetHeight; + int barY = (height - touchTargetHeight) / 2; int seekLeft = getPaddingLeft(); int seekRight = width - getPaddingRight(); int progressY = barY + (touchTargetHeight - barHeight) / 2; From 785fc761243720598b0b995bfda0dbbd5e361326 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Wed, 10 May 2017 22:17:39 -0700 Subject: [PATCH 033/220] Delete gcore_versions.bzl --- extensions/cronet/src/main/gcore_versions.bzl | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 extensions/cronet/src/main/gcore_versions.bzl diff --git a/extensions/cronet/src/main/gcore_versions.bzl b/extensions/cronet/src/main/gcore_versions.bzl deleted file mode 100644 index 7f9f9c3863..0000000000 --- a/extensions/cronet/src/main/gcore_versions.bzl +++ /dev/null @@ -1,5 +0,0 @@ -"""GCore versions supporting Cronet.""" -GCORE_VERSIONS = [ - "v10", -] - From 37ffb33183ca407463772184758799727b689988 Mon Sep 17 00:00:00 2001 From: arakawa_yusuke Date: Thu, 11 May 2017 21:11:21 +0900 Subject: [PATCH 034/220] Refactor: Change the position of variable. --- .../java/com/google/android/exoplayer2/demo/PlayerActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 6cc9cabfc0..bab51ef6a4 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -234,7 +234,6 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay Intent intent = getIntent(); boolean needNewPlayer = player == null; if (needNewPlayer) { - boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false); UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA) ? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null; DrmSessionManager drmSessionManager = null; @@ -253,6 +252,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } } + boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false); @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode = ((DemoApplication) getApplication()).useExtensionRenderers() ? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER From ed65958e5d114a74676ee63e23b74fd2790e8c7b Mon Sep 17 00:00:00 2001 From: sillywab8 Date: Fri, 12 May 2017 08:24:40 -0500 Subject: [PATCH 035/220] Add support for AVC Level 5.2 --- .../com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java | 1 + 1 file changed, 1 insertion(+) 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 a09f6e26dd..2bb3603df9 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 @@ -429,6 +429,7 @@ public final class MediaCodecUtil { case CodecProfileLevel.AVCLevel42: return 8704 * 16 * 16; case CodecProfileLevel.AVCLevel5: return 22080 * 16 * 16; case CodecProfileLevel.AVCLevel51: return 36864 * 16 * 16; + case CodecProfileLevel.AVCLevel52: return 36864 * 16 * 16; default: return -1; } } From 3dd2f4af573addd561f80991e72ab3ce6c834d5a Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 11 May 2017 02:31:13 -0700 Subject: [PATCH 036/220] Tests for timeline repeat mode support. Checking the expected next/previous window indices and if the correct window or period gets returned. TimelineTest defines mock classes and verification methods used by the specific implementation tests. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155727385 --- .../android/exoplayer2/TimelineTest.java | 251 ++++++++++++++++++ .../source/ClippingMediaSourceTest.java | 60 ++--- .../source/ConcatenatingMediaSourceTest.java | 111 ++++++++ .../source/LoopingMediaSourceTest.java | 91 +++++++ 4 files changed, 475 insertions(+), 38 deletions(-) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java new file mode 100644 index 0000000000..fc3ccacbf2 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java @@ -0,0 +1,251 @@ +/* + * 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; + +import com.google.android.exoplayer2.ExoPlayer.RepeatMode; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.Listener; +import com.google.android.exoplayer2.upstream.Allocator; +import java.io.IOException; +import junit.framework.TestCase; + +/** + * Unit test for {@link Timeline}. + */ +public class TimelineTest extends TestCase { + + /** + * Fake timeline with multiple periods and user-defined window id. + */ + public static final class FakeTimeline extends Timeline { + + private static final int WINDOW_DURATION_US = 1000000; + + private final int periodCount; + private final int id; + + public FakeTimeline(int periodCount, int id) { + this.periodCount = periodCount; + this.id = id; + } + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + return window.set(id, 0, 0, true, false, 0, WINDOW_DURATION_US, 0, periodCount - 1, 0); + } + + @Override + public int getPeriodCount() { + return periodCount; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + return period.set(new int[] { id, periodIndex }, null, 0, WINDOW_DURATION_US, 0, false); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return C.INDEX_UNSET; + } + } + + /** + * Returns a stub {@link MediaSource} with the specified {@link Timeline} in its source info. + */ + public static MediaSource stubMediaSourceSourceWithTimeline(final Timeline timeline) { + return new MediaSource() { + @Override + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + listener.onSourceInfoRefreshed(timeline, null); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + } + + @Override + public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + return null; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + } + + @Override + public void releaseSource() { + } + }; + } + + /** + * Works in conjunction with {@code stubMediaSourceSourceWithTimeline} to extract the Timeline + * from a media source. + */ + public static Timeline extractTimelineFromMediaSource(MediaSource mediaSource) { + class TimelineListener implements Listener { + private Timeline timeline; + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + this.timeline = timeline; + } + } + TimelineListener listener = new TimelineListener(); + mediaSource.prepareSource(null, true, listener); + return listener.timeline; + } + + /** + * Verify the behaviour of {@link Timeline#getNextWindowIndex(int, int)}, + * {@link Timeline#getPreviousWindowIndex(int, int)}, + * {@link Timeline#getWindow(int, Window, boolean)}, + * {@link Timeline#getNextPeriodIndex(int, Period, Window, int)}, and + * {@link Timeline#getPeriod(int, Period, boolean)}. + */ + public static final class TimelineVerifier { + + private final Timeline timeline; + + public TimelineVerifier(Timeline timeline) { + this.timeline = timeline; + } + + public TimelineVerifier assertWindowIds(int... expectedWindowIds) { + Window window = new Window(); + assertEquals(expectedWindowIds.length, timeline.getWindowCount()); + for (int i = 0; i < timeline.getWindowCount(); i++) { + timeline.getWindow(i, window, true); + assertEquals(expectedWindowIds[i], window.id); + } + return this; + } + + public TimelineVerifier assertPreviousWindowIndices(@RepeatMode int repeatMode, + int... expectedPreviousWindowIndices) { + for (int i = 0; i < timeline.getWindowCount(); i++) { + assertEquals(expectedPreviousWindowIndices[i], + timeline.getPreviousWindowIndex(i, repeatMode)); + } + return this; + } + + public TimelineVerifier assertNextWindowIndices(@RepeatMode int repeatMode, + int... expectedNextWindowIndices) { + for (int i = 0; i < timeline.getWindowCount(); i++) { + assertEquals(expectedNextWindowIndices[i], + timeline.getNextWindowIndex(i, repeatMode)); + } + return this; + } + + public TimelineVerifier assertPeriodCounts(int... expectedPeriodCounts) { + int windowCount = timeline.getWindowCount(); + int[] accumulatedPeriodCounts = new int[windowCount + 1]; + accumulatedPeriodCounts[0] = 0; + for (int i = 0; i < windowCount; i++) { + accumulatedPeriodCounts[i + 1] = accumulatedPeriodCounts[i] + expectedPeriodCounts[i]; + } + assertEquals(accumulatedPeriodCounts[accumulatedPeriodCounts.length - 1], + timeline.getPeriodCount()); + Window window = new Window(); + Period period = new Period(); + for (int i = 0; i < windowCount; i++) { + timeline.getWindow(i, window, true); + assertEquals(accumulatedPeriodCounts[i], window.firstPeriodIndex); + assertEquals(accumulatedPeriodCounts[i + 1] - 1, window.lastPeriodIndex); + } + int expectedWindowIndex = 0; + for (int i = 0; i < timeline.getPeriodCount(); i++) { + timeline.getPeriod(i, period, true); + while (i >= accumulatedPeriodCounts[expectedWindowIndex + 1]) { + expectedWindowIndex++; + } + assertEquals(expectedWindowIndex, period.windowIndex); + assertEquals(i - accumulatedPeriodCounts[expectedWindowIndex], ((int[]) period.id)[1]); + if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) { + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, + ExoPlayer.REPEAT_MODE_OFF)); + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, + ExoPlayer.REPEAT_MODE_ONE)); + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, + ExoPlayer.REPEAT_MODE_ALL)); + } else { + int nextWindowOff = timeline.getNextWindowIndex(expectedWindowIndex, + ExoPlayer.REPEAT_MODE_OFF); + int nextWindowOne = timeline.getNextWindowIndex(expectedWindowIndex, + ExoPlayer.REPEAT_MODE_ONE); + int nextWindowAll = timeline.getNextWindowIndex(expectedWindowIndex, + ExoPlayer.REPEAT_MODE_ALL); + int nextPeriodOff = nextWindowOff == C.INDEX_UNSET ? C.INDEX_UNSET + : accumulatedPeriodCounts[nextWindowOff]; + int nextPeriodOne = nextWindowOne == C.INDEX_UNSET ? C.INDEX_UNSET + : accumulatedPeriodCounts[nextWindowOne]; + int nextPeriodAll = nextWindowAll == C.INDEX_UNSET ? C.INDEX_UNSET + : accumulatedPeriodCounts[nextWindowAll]; + assertEquals(nextPeriodOff, timeline.getNextPeriodIndex(i, period, window, + ExoPlayer.REPEAT_MODE_OFF)); + assertEquals(nextPeriodOne, timeline.getNextPeriodIndex(i, period, window, + ExoPlayer.REPEAT_MODE_ONE)); + assertEquals(nextPeriodAll, timeline.getNextPeriodIndex(i, period, window, + ExoPlayer.REPEAT_MODE_ALL)); + } + } + return this; + } + } + + public void testEmptyTimeline() { + new TimelineVerifier(Timeline.EMPTY) + .assertWindowIds() + .assertPeriodCounts(); + } + + public void testSinglePeriodTimeline() { + Timeline timeline = new FakeTimeline(1, 111); + new TimelineVerifier(timeline) + .assertWindowIds(111) + .assertPeriodCounts(1) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0); + } + + public void testMultiPeriodTimeline() { + Timeline timeline = new FakeTimeline(5, 111); + new TimelineVerifier(timeline) + .assertWindowIds(111) + .assertPeriodCounts(5) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0); + } +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 0933fb858b..f570272bef 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -15,20 +15,15 @@ */ package com.google.android.exoplayer2.source; -import static org.mockito.Mockito.doAnswer; - import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; -import com.google.android.exoplayer2.source.MediaSource.Listener; -import com.google.android.exoplayer2.testutil.TestUtil; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; +import com.google.android.exoplayer2.TimelineTest; +import com.google.android.exoplayer2.TimelineTest.FakeTimeline; +import com.google.android.exoplayer2.TimelineTest.TimelineVerifier; /** * Unit tests for {@link ClippingMediaSource}. @@ -38,15 +33,11 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { private static final long TEST_PERIOD_DURATION_US = 1000000; private static final long TEST_CLIP_AMOUNT_US = 300000; - @Mock - private MediaSource mockMediaSource; - private Timeline clippedTimeline; private Window window; private Period period; @Override protected void setUp() throws Exception { - TestUtil.setUpMockito(this); window = new Timeline.Window(); period = new Timeline.Period(); } @@ -109,35 +100,28 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { clippedTimeline.getPeriod(0, period).getDurationUs()); } + public void testWindowAndPeriodIndices() { + Timeline timeline = new FakeTimeline(1, 111); + Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, + TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); + new TimelineVerifier(clippedTimeline) + .assertWindowIds(111) + .assertPeriodCounts(1) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0); + } + /** * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline. */ - private Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { - mockMediaSourceSourceWithTimeline(timeline); - new ClippingMediaSource(mockMediaSource, startMs, endMs).prepareSource(null, true, - new Listener() { - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - clippedTimeline = timeline; - } - }); - return clippedTimeline; - } - - /** - * Returns a mock {@link MediaSource} with the specified {@link Timeline} in its source info. - */ - private MediaSource mockMediaSourceSourceWithTimeline(final Timeline timeline) { - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - MediaSource.Listener listener = (MediaSource.Listener) invocation.getArguments()[2]; - listener.onSourceInfoRefreshed(timeline, null); - return null; - } - }).when(mockMediaSource).prepareSource(Mockito.any(ExoPlayer.class), Mockito.anyBoolean(), - Mockito.any(MediaSource.Listener.class)); - return mockMediaSource; + private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { + MediaSource mediaSource = TimelineTest.stubMediaSourceSourceWithTimeline(timeline); + return TimelineTest.extractTimelineFromMediaSource( + new ClippingMediaSource(mediaSource, startMs, endMs)); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java new file mode 100644 index 0000000000..08d2c1cda7 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.TimelineTest; +import com.google.android.exoplayer2.TimelineTest.FakeTimeline; +import com.google.android.exoplayer2.TimelineTest.TimelineVerifier; +import junit.framework.TestCase; + +/** + * Unit tests for {@link ConcatenatingMediaSource}. + */ +public final class ConcatenatingMediaSourceTest extends TestCase { + + public void testSingleMediaSource() { + Timeline timeline = getConcatenatedTimeline(false, new FakeTimeline(3, 111)); + new TimelineVerifier(timeline) + .assertWindowIds(111) + .assertPeriodCounts(3) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0); + + timeline = getConcatenatedTimeline(true, new FakeTimeline(3, 111)); + new TimelineVerifier(timeline) + .assertWindowIds(111) + .assertPeriodCounts(3) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0); + } + + public void testMultipleMediaSources() { + Timeline[] timelines = { new FakeTimeline(3, 111), new FakeTimeline(1, 222), + new FakeTimeline(3, 333) }; + Timeline timeline = getConcatenatedTimeline(false, timelines); + new TimelineVerifier(timeline) + .assertWindowIds(111, 222, 333) + .assertPeriodCounts(3, 1, 3) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); + + timeline = getConcatenatedTimeline(true, timelines); + new TimelineVerifier(timeline) + .assertWindowIds(111, 222, 333) + .assertPeriodCounts(3, 1, 3) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 2, 0, 1) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 1, 2, 0) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); + } + + public void testNestedMediaSources() { + Timeline timeline = getConcatenatedTimeline(false, + getConcatenatedTimeline(false, new FakeTimeline(1, 111), new FakeTimeline(1, 222)), + getConcatenatedTimeline(true, new FakeTimeline(1, 333), new FakeTimeline(1, 444))); + new TimelineVerifier(timeline) + .assertWindowIds(111, 222, 333, 444) + .assertPeriodCounts(1, 1, 1, 1) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1, 2) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 3, 2) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 3, 0, 1, 2) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, 1, 2, 3, C.INDEX_UNSET) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 3, 2) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 1, 2, 3, 0); + } + + /** + * Wraps the specified timelines in a {@link ConcatenatingMediaSource} and returns + * the concatenated timeline. + */ + private static Timeline getConcatenatedTimeline(boolean isRepeatOneAtomic, + Timeline... timelines) { + MediaSource[] mediaSources = new MediaSource[timelines.length]; + for (int i = 0; i < timelines.length; i++) { + mediaSources[i] = TimelineTest.stubMediaSourceSourceWithTimeline(timelines[i]); + } + return TimelineTest.extractTimelineFromMediaSource( + new ConcatenatingMediaSource(isRepeatOneAtomic, mediaSources)); + } + + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java new file mode 100644 index 0000000000..7b4a449764 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -0,0 +1,91 @@ +/* + * 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.source; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.TimelineTest; +import com.google.android.exoplayer2.TimelineTest.FakeTimeline; +import com.google.android.exoplayer2.TimelineTest.TimelineVerifier; +import junit.framework.TestCase; + +/** + * Unit tests for {@link LoopingMediaSource}. + */ +public class LoopingMediaSourceTest extends TestCase { + + private final Timeline multiWindowTimeline; + + public LoopingMediaSourceTest() { + multiWindowTimeline = TimelineTest.extractTimelineFromMediaSource( + new ConcatenatingMediaSource( + TimelineTest.stubMediaSourceSourceWithTimeline(new FakeTimeline(1, 111)), + TimelineTest.stubMediaSourceSourceWithTimeline(new FakeTimeline(1, 222)), + TimelineTest.stubMediaSourceSourceWithTimeline(new FakeTimeline(1, 333)))); + } + + public void testSingleLoop() { + Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1); + new TimelineVerifier(timeline) + .assertWindowIds(111, 222, 333) + .assertPeriodCounts(1, 1, 1) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); + } + + public void testMultiLoop() { + Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3); + new TimelineVerifier(timeline) + .assertWindowIds(111, 222, 333, 111, 222, 333, 111, 222, 333) + .assertPeriodCounts(1, 1, 1, 1, 1, 1, 1, 1, 1) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, + C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2, 3, 4, 5, 6, 7, 8) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 8, 0, 1, 2, 3, 4, 5, 6, 7) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, 1, 2, 3, 4, 5, 6, 7, 8, C.INDEX_UNSET) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2, 3, 4, 5, 6, 7, 8) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 1, 2, 3, 4, 5, 6, 7, 8, 0); + } + + public void testInfiniteLoop() { + Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE); + new TimelineVerifier(timeline) + .assertWindowIds(111, 222, 333) + .assertPeriodCounts(1, 1, 1) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, 2, 0, 1) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2) + .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, 1, 2, 0) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2) + .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); + } + + /** + * Wraps the specified timeline in a {@link LoopingMediaSource} and returns + * the looping timeline. + */ + private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) { + MediaSource mediaSource = TimelineTest.stubMediaSourceSourceWithTimeline(timeline); + return TimelineTest.extractTimelineFromMediaSource( + new LoopingMediaSource(mediaSource, loopCount)); + } + +} From f335fb936d223fcbb8822ef6ff3f45b31dd72f53 Mon Sep 17 00:00:00 2001 From: bachinger Date: Sun, 14 May 2017 22:26:39 -0700 Subject: [PATCH 037/220] Enlarge size of data array of parsable packetArray if ogg packet size exceeds the current size. https://github.com/google/ExoPlayer/issues/2782 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156018137 --- .../assets/ogg/bear_vorbis.ogg.0.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.1.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.2.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.3.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.unklen.dump | 2 +- .../exoplayer2/extractor/ogg/OggPacket.java | 19 +++++++++++++++++-- .../extractor/ogg/StreamReader.java | 5 ++--- .../extractor/ogg/VorbisReader.java | 2 +- 8 files changed, 25 insertions(+), 11 deletions(-) diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump index 536f76adad..8e2c5125a3 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/vorbis - maxInputSize = 65025 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump index 7490773bd5..aa25303ac3 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/vorbis - maxInputSize = 65025 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump index 82ad16e701..58969058fa 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/vorbis - maxInputSize = 65025 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump index 810b66901c..4c789a8431 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/vorbis - maxInputSize = 65025 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump index 8e86ca340d..2f163572bf 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/vorbis - maxInputSize = 65025 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java index 892f0a68af..c7f4e9489b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; +import java.util.Arrays; /** * OGG packet class. @@ -27,8 +28,8 @@ import java.io.IOException; /* package */ final class OggPacket { private final OggPageHeader pageHeader = new OggPageHeader(); - private final ParsableByteArray packetArray = - new ParsableByteArray(new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0); + private final ParsableByteArray packetArray = new ParsableByteArray( + new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0); private int currentSegmentIndex = C.INDEX_UNSET; private int segmentCount; @@ -85,6 +86,9 @@ import java.io.IOException; int size = calculatePacketSize(currentSegmentIndex); int segmentIndex = currentSegmentIndex + segmentCount; if (size > 0) { + if (packetArray.capacity() < packetArray.limit() + size) { + packetArray.data = Arrays.copyOf(packetArray.data, packetArray.limit() + size); + } input.readFully(packetArray.data, packetArray.limit(), size); packetArray.setLimit(packetArray.limit() + size); populated = pageHeader.laces[segmentIndex - 1] != 255; @@ -118,6 +122,17 @@ import java.io.IOException; return packetArray; } + /** + * Trims the packet data array. + */ + public void trimPayload() { + if (packetArray.data.length == OggPageHeader.MAX_PAGE_PAYLOAD) { + return; + } + packetArray.data = Arrays.copyOf(packetArray.data, Math.max(OggPageHeader.MAX_PAGE_PAYLOAD, + packetArray.limit())); + } + /** * Calculates the size of the packet starting from {@code startSegmentIndex}. * 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 6424155bd9..c203b0c6bd 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 @@ -103,15 +103,12 @@ import java.io.IOException; switch (state) { case STATE_READ_HEADERS: return readHeaders(input); - case STATE_SKIP_HEADERS: input.skipFully((int) payloadStartPosition); state = STATE_READ_PAYLOAD; return Extractor.RESULT_CONTINUE; - case STATE_READ_PAYLOAD: return readPayload(input, seekPosition); - default: // Never happens. throw new IllegalStateException(); @@ -152,6 +149,8 @@ import java.io.IOException; setupData = null; state = STATE_READ_PAYLOAD; + // First payload packet. Trim the payload array of the ogg packet after headers have been read. + oggPacket.trimPayload(); return Extractor.RESULT_CONTINUE; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java index ae0a69ef7d..31ac6858be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java @@ -101,7 +101,7 @@ import java.util.ArrayList; codecInitialisationData.add(vorbisSetup.setupHeaderData); setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, null, - this.vorbisSetup.idHeader.bitrateNominal, OggPageHeader.MAX_PAGE_PAYLOAD, + this.vorbisSetup.idHeader.bitrateNominal, Format.NO_VALUE, this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate, codecInitialisationData, null, 0, null); return true; From 929ef172a01d5f253803680af6ed6711b21e4afd Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 15 May 2017 09:12:05 -0700 Subject: [PATCH 038/220] Fix handling of removed periods If a timeline update removed periods at the end of the timeline which had been buffered, handleSourceInfoRefreshed would call getNextPeriodIndex and get back -1 for the last period holder in the new timeline. Then isLastPeriod(-1) could throw. Fix this behavior so that the remainder of the timeline is discarded. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156061016 --- .../google/android/exoplayer2/ExoPlayerImplInternal.java | 7 +++---- 1 file changed, 3 insertions(+), 4 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 35ce0e1453..0f0b18c7b4 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 @@ -1016,11 +1016,10 @@ import java.io.IOException; MediaPeriodHolder previousPeriodHolder = periodHolder; periodHolder = periodHolder.next; periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode); - boolean isLastPeriod = isLastPeriod(periodIndex); - timeline.getPeriod(periodIndex, period, true); - if (periodHolder.uid.equals(period.uid)) { + if (periodIndex != C.INDEX_UNSET + && periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) { // The holder is consistent with the new timeline. Update its index and continue. - periodHolder.setIndex(periodIndex, isLastPeriod); + periodHolder.setIndex(periodIndex, isLastPeriod(periodIndex)); seenReadingPeriod |= (periodHolder == readingPeriodHolder); } else { // The holder is inconsistent with the new timeline. From 9a7306a4cc219feb33f24157629b8f8f393ddebb Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 15 May 2017 09:15:29 -0700 Subject: [PATCH 039/220] Remove unnecessary throws clause ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156061458 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8fb9bc9271..f16bf07c26 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 @@ -237,7 +237,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } @Override - public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { + public final int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_NOT_SEAMLESS; } From b31ec337ed60143248038a79a2c16d35516b76c3 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 15 May 2017 11:14:04 -0700 Subject: [PATCH 040/220] Enable neon for libvpx on arm64-v8a ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156077889 --- extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 15dbabdb1f..5f058d0551 100755 --- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh +++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh @@ -51,8 +51,7 @@ 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 --disable-neon" -config[4]+=" --disable-neon-asm" +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" From 84875bbe7b380bdd4f797c22d53650321836ce6f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 15 May 2017 15:36:20 -0700 Subject: [PATCH 041/220] Constrain DefaultTimeBar maximum positions ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156113616 --- .../google/android/exoplayer2/ui/DefaultTimeBar.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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 06ecb1aa69..fd05fdd5d0 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 @@ -465,12 +465,10 @@ public class DefaultTimeBar extends View implements TimeBar { scrubberBar.set(progressBar); long newScrubberTime = scrubbing ? scrubPosition : position; if (duration > 0) { - int bufferedPixelWidth = - (int) ((progressBar.width() * bufferedPosition) / duration); - bufferedBar.right = progressBar.left + bufferedPixelWidth; - int scrubberPixelPosition = - (int) ((progressBar.width() * newScrubberTime) / duration); - scrubberBar.right = progressBar.left + scrubberPixelPosition; + int bufferedPixelWidth = (int) ((progressBar.width() * bufferedPosition) / duration); + bufferedBar.right = Math.min(progressBar.left + bufferedPixelWidth, progressBar.right); + int scrubberPixelPosition = (int) ((progressBar.width() * newScrubberTime) / duration); + scrubberBar.right = Math.min(progressBar.left + scrubberPixelPosition, progressBar.right); } else { bufferedBar.right = progressBar.left; scrubberBar.right = progressBar.left; From 72ba736a7f87c7f5eed8b5d00ddfa69f6574bef3 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 15 May 2017 18:06:20 -0700 Subject: [PATCH 042/220] Clear up BaseRenderer.disable - Call onDisabled last. onDisabled really shouldn't be doing anything with the stream, so pretty sure this is fine (and guarantees the stream is cleared properly even if onDisabled throws a RTE). - Remove super.onDisabled calls from Text/Metadata renderers. This is just for consistency; we don't make such calls in other direct descendants of BaseRenderer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156130640 --- .../main/java/com/google/android/exoplayer2/BaseRenderer.java | 4 +--- .../google/android/exoplayer2/metadata/MetadataRenderer.java | 1 - .../java/com/google/android/exoplayer2/text/TextRenderer.java | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 44fb6d68ae..396584a39e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -142,9 +142,9 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { public final void disable() { Assertions.checkState(state == STATE_ENABLED); state = STATE_DISABLED; - onDisabled(); stream = null; streamIsFinal = false; + onDisabled(); } // RendererCapabilities implementation. @@ -300,8 +300,6 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { /** * Returns whether the upstream source is ready. - * - * @return Whether the source is ready. */ protected final boolean isSourceReady() { return readEndOfStream ? streamIsFinal : stream.isReady(); 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 814238970b..70b2d8aab9 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 @@ -153,7 +153,6 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { protected void onDisabled() { flushPendingMetadata(); decoder = null; - super.onDisabled(); } @Override 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 2f07fe5294..4950549b19 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 @@ -254,7 +254,6 @@ public final class TextRenderer extends BaseRenderer implements Callback { streamFormat = null; clearOutput(); releaseDecoder(); - super.onDisabled(); } @Override From 8e0bf6cd2f26d2b9a2f794fb140b2e9a14ec04f0 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 15 May 2017 18:11:45 -0700 Subject: [PATCH 043/220] Clear the correct buffer in MediaCodecRenderer ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156131086 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f16bf07c26..d58dbc4065 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 @@ -488,7 +488,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } if (format == null) { // We don't have a format yet, so try and read one. - buffer.clear(); + flagsOnlyBuffer.clear(); int result = readSource(formatHolder, flagsOnlyBuffer, true); if (result == C.RESULT_FORMAT_READ) { onInputFormatChanged(formatHolder.format); From 1594e71917a37ce2f603cb462403b884023eebfd Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 16 May 2017 04:58:14 -0700 Subject: [PATCH 044/220] Test for changing repeat mode during playback (Related to GitHub Issue #2577) Added test to ExoPlayerTest which changes the repeat mode during playback. Test verifies that ExoPlayer shows the periods in the intended order. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156168166 --- .../android/exoplayer2/ExoPlayerTest.java | 69 ++++++++++++++++++- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 00eba6b52b..2d4ff98947 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -218,10 +218,74 @@ public final class ExoPlayerTest extends TestCase { Pair.create(timeline, thirdSourceManifest)); } + public void testRepeatModeChanges() throws Exception { + Timeline timeline = new FakeTimeline( + new TimelineWindowDefinition(true, false, 100000), + new TimelineWindowDefinition(true, false, 100000), + new TimelineWindowDefinition(true, false, 100000)); + final int[] actionSchedule = { // 0 -> 1 + ExoPlayer.REPEAT_MODE_ONE, // 1 -> 1 + ExoPlayer.REPEAT_MODE_OFF, // 1 -> 2 + -1, // 2 -> ended + ExoPlayer.REPEAT_MODE_ONE, // ended -> 2 + ExoPlayer.REPEAT_MODE_ALL, // 2 -> 0 + ExoPlayer.REPEAT_MODE_ONE, // 0 -> 0 + -1, // 0 -> 0 + ExoPlayer.REPEAT_MODE_OFF, // 0 -> 1 + -1, // 1 -> 2 + -1, // 2 -> ended + -1 + }; + int[] expectedWindowIndices = {1, 1, 2, 2, 2, 0, 0, 0, 1, 2, 2}; + final LinkedList windowIndices = new LinkedList<>(); + final CountDownLatch actionCounter = new CountDownLatch(actionSchedule.length); + PlayerWrapper playerWrapper = new PlayerWrapper() { + @SuppressWarnings("ResourceType") + private void executeAction() { + int actionIndex = actionSchedule.length - (int) actionCounter.getCount(); + if (actionSchedule[actionIndex] != -1) { + player.setRepeatMode(actionSchedule[actionIndex]); + } + windowIndices.add(player.getCurrentWindowIndex()); + actionCounter.countDown(); + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + super.onPlayerStateChanged(playWhenReady, playbackState); + if (playbackState == ExoPlayer.STATE_ENDED) { + executeAction(); + } + } + + @Override + public void onPositionDiscontinuity() { + super.onPositionDiscontinuity(); + executeAction(); + } + }; + MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT); + FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT); + playerWrapper.setup(mediaSource, renderer); + boolean finished = actionCounter.await(TIMEOUT_MS, TimeUnit.MILLISECONDS); + playerWrapper.release(); + assertTrue("Test playback timed out waiting for action schedule to end.", finished); + if (playerWrapper.exception != null) { + throw playerWrapper.exception; + } + assertEquals(expectedWindowIndices.length, windowIndices.size()); + for (int i = 0; i < expectedWindowIndices.length; i++) { + assertEquals(expectedWindowIndices[i], windowIndices.get(i).intValue()); + } + assertEquals(9, playerWrapper.positionDiscontinuityCount); + assertTrue(renderer.isEnded); + playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null)); + } + /** * Wraps a player with its own handler thread. */ - private static final class PlayerWrapper implements ExoPlayer.EventListener { + private static class PlayerWrapper implements ExoPlayer.EventListener { private final CountDownLatch sourceInfoCountDownLatch; private final CountDownLatch endedCountDownLatch; @@ -229,7 +293,7 @@ public final class ExoPlayerTest extends TestCase { private final Handler handler; private final LinkedList> sourceInfos; - private ExoPlayer player; + /* package */ ExoPlayer player; private TrackGroupArray trackGroups; private Exception exception; @@ -580,7 +644,6 @@ public final class ExoPlayerTest extends TestCase { @Override public long seekToUs(long positionUs) { assertTrue(preparedPeriod); - assertEquals(0, positionUs); return positionUs; } From 4359d44331b6269b5516737a387bc41a6b99201d Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 16 May 2017 08:39:03 -0700 Subject: [PATCH 045/220] Rename CronetEngineFactory to CronetEngineWrapper In addition, the class now accepts available Cronet instances and returns the source of the current CronetEngine. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156185701 --- .../ext/cronet/CronetDataSourceFactory.java | 45 ++-- .../ext/cronet/CronetEngineFactory.java | 170 ------------- .../ext/cronet/CronetEngineWrapper.java | 238 ++++++++++++++++++ 3 files changed, 260 insertions(+), 193 deletions(-) delete mode 100644 extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineFactory.java create mode 100644 extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java 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 2e4c27a920..d6237fc988 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 @@ -43,7 +43,7 @@ public final class CronetDataSourceFactory extends BaseFactory { public static final int DEFAULT_READ_TIMEOUT_MILLIS = CronetDataSource.DEFAULT_READ_TIMEOUT_MILLIS; - private final CronetEngineFactory cronetEngineFactory; + private final CronetEngineWrapper cronetEngineWrapper; private final Executor executor; private final Predicate contentTypePredicate; private final TransferListener transferListener; @@ -55,14 +55,14 @@ public final class CronetDataSourceFactory extends BaseFactory { /** * Constructs a CronetDataSourceFactory. *

      - * If the {@link CronetEngineFactory} fails to provide a suitable {@link CronetEngine}, the - * provided fallback {@link HttpDataSource.Factory} will be used instead. + * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided + * fallback {@link HttpDataSource.Factory} will be used instead. * * Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link * CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables * cross-protocol redirects. * - * @param cronetEngineFactory A {@link CronetEngineFactory}. + * @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 @@ -71,25 +71,25 @@ public final class CronetDataSourceFactory extends BaseFactory { * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case * no suitable CronetEngine can be build. */ - public CronetDataSourceFactory(CronetEngineFactory cronetEngineFactory, + public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper, Executor executor, Predicate contentTypePredicate, TransferListener transferListener, HttpDataSource.Factory fallbackFactory) { - this(cronetEngineFactory, executor, contentTypePredicate, transferListener, + this(cronetEngineWrapper, executor, contentTypePredicate, transferListener, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory); } /** * Constructs a CronetDataSourceFactory. *

      - * If the {@link CronetEngineFactory} fails to provide a suitable {@link CronetEngine}, a + * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a * {@link DefaultHttpDataSourceFactory} will be used instead. * * Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link * CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables * cross-protocol redirects. * - * @param cronetEngineFactory A {@link CronetEngineFactory}. + * @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 @@ -97,10 +97,10 @@ public final class CronetDataSourceFactory extends BaseFactory { * @param transferListener An optional listener. * @param userAgent A user agent used to create a fallback HttpDataSource if needed. */ - public CronetDataSourceFactory(CronetEngineFactory cronetEngineFactory, + public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper, Executor executor, Predicate contentTypePredicate, TransferListener transferListener, String userAgent) { - this(cronetEngineFactory, executor, contentTypePredicate, transferListener, + 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)); @@ -109,10 +109,10 @@ public final class CronetDataSourceFactory extends BaseFactory { /** * Constructs a CronetDataSourceFactory. *

      - * If the {@link CronetEngineFactory} fails to provide a suitable {@link CronetEngine}, a + * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a * {@link DefaultHttpDataSourceFactory} will be used instead. * - * @param cronetEngineFactory A {@link CronetEngineFactory}. + * @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 @@ -123,11 +123,11 @@ public final class CronetDataSourceFactory extends BaseFactory { * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. * @param userAgent A user agent used to create a fallback HttpDataSource if needed. */ - public CronetDataSourceFactory(CronetEngineFactory cronetEngineFactory, + public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper, Executor executor, Predicate contentTypePredicate, TransferListener transferListener, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, String userAgent) { - this(cronetEngineFactory, executor, contentTypePredicate, transferListener, + this(cronetEngineWrapper, executor, contentTypePredicate, transferListener, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects, new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects)); @@ -136,10 +136,10 @@ public final class CronetDataSourceFactory extends BaseFactory { /** * Constructs a CronetDataSourceFactory. *

      - * If the {@link CronetEngineFactory} fails to provide a suitable {@link CronetEngine}, the - * provided fallback {@link HttpDataSource.Factory} will be used instead. + * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided + * fallback {@link HttpDataSource.Factory} will be used instead. * - * @param cronetEngineFactory A {@link CronetEngineFactory}. + * @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 @@ -151,12 +151,12 @@ public final class CronetDataSourceFactory extends BaseFactory { * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case * no suitable CronetEngine can be build. */ - public CronetDataSourceFactory(CronetEngineFactory cronetEngineFactory, + public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper, Executor executor, Predicate contentTypePredicate, TransferListener transferListener, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, HttpDataSource.Factory fallbackFactory) { - this.cronetEngineFactory = cronetEngineFactory; + this.cronetEngineWrapper = cronetEngineWrapper; this.executor = executor; this.contentTypePredicate = contentTypePredicate; this.transferListener = transferListener; @@ -169,13 +169,12 @@ public final class CronetDataSourceFactory extends BaseFactory { @Override protected HttpDataSource createDataSourceInternal(HttpDataSource.RequestProperties defaultRequestProperties) { - CronetEngine cronetEngine = cronetEngineFactory.createCronetEngine(); + CronetEngine cronetEngine = cronetEngineWrapper.getCronetEngine(); if (cronetEngine == null) { return fallbackFactory.createDataSource(); } - return new CronetDataSource(cronetEngineFactory.createCronetEngine(), executor, - contentTypePredicate, transferListener, connectTimeoutMs, readTimeoutMs, - resetTimeoutOnRedirects, defaultRequestProperties); + return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener, + connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties); } } diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineFactory.java deleted file mode 100644 index 7211ea64f4..0000000000 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineFactory.java +++ /dev/null @@ -1,170 +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.ext.cronet; - -import android.content.Context; -import android.util.Log; -import java.lang.reflect.Field; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import org.chromium.net.CronetEngine; -import org.chromium.net.CronetProvider; - -/** - * A factory class which creates or reuses a {@link CronetEngine}. - */ -public final class CronetEngineFactory { - - private static final String TAG = "CronetEngineFactory"; - - private final Context context; - private final boolean preferGMSCoreCronet; - - private CronetEngine cronetEngine = null; - - /** - * Creates the factory for a {@link CronetEngine}. Sets factory to prefer natively bundled Cronet - * over GMSCore Cronet if both are available. - * - * @param context A context. - */ - public CronetEngineFactory(Context context) { - this(context, false); - } - - /** - * Creates the factory for a {@link CronetEngine} and specifies whether Cronet from GMSCore should - * be preferred over natively bundled Cronet if both are available. - * - * @param context A context. - */ - public CronetEngineFactory(Context context, boolean preferGMSCoreCronet) { - this.context = context.getApplicationContext(); - this.preferGMSCoreCronet = preferGMSCoreCronet; - } - - /** - * Create or reuse a {@link CronetEngine}. If no CronetEngine is available, the method returns - * null. - * - * @return The CronetEngine, or null if no CronetEngine is available. - */ - /* package */ CronetEngine createCronetEngine() { - if (cronetEngine == null) { - List cronetProviders = CronetProvider.getAllProviders(context); - // Remove disabled and fallback Cronet providers from list - for (int i = cronetProviders.size() - 1; i >= 0; i--) { - if (!cronetProviders.get(i).isEnabled() - || CronetProvider.PROVIDER_NAME_FALLBACK.equals(cronetProviders.get(i).getName())) { - cronetProviders.remove(i); - } - } - // Sort remaining providers by type and version. - Collections.sort(cronetProviders, new CronetProviderComparator(preferGMSCoreCronet)); - for (int i = 0; i < cronetProviders.size(); i++) { - String providerName = cronetProviders.get(i).getName(); - try { - cronetEngine = cronetProviders.get(i).createBuilder().build(); - Log.d(TAG, "CronetEngine built using " + providerName); - } catch (UnsatisfiedLinkError e) { - Log.w(TAG, "Failed to link Cronet binaries. Please check if native Cronet binaries are " - + "bundled into your app."); - } - } - } - if (cronetEngine == null) { - Log.w(TAG, "Cronet not available. Using fallback provider."); - } - return cronetEngine; - } - - private static class CronetProviderComparator implements Comparator { - - private final String gmsCoreCronetName; - private final boolean preferGMSCoreCronet; - - public CronetProviderComparator(boolean preferGMSCoreCronet) { - // GMSCore CronetProvider classes are only available in some configurations. - // Thus, we use reflection to copy static name. - String gmsCoreVersionString = null; - try { - Class cronetProviderInstallerClass = - Class.forName("com.google.android.gms.net.CronetProviderInstaller"); - Field providerNameField = cronetProviderInstallerClass.getDeclaredField("PROVIDER_NAME"); - gmsCoreVersionString = (String) providerNameField.get(null); - } catch (ClassNotFoundException e) { - // GMSCore CronetProvider not available. - } catch (NoSuchFieldException e) { - // GMSCore CronetProvider not available. - } catch (IllegalAccessException e) { - // GMSCore CronetProvider not available. - } - gmsCoreCronetName = gmsCoreVersionString; - this.preferGMSCoreCronet = preferGMSCoreCronet; - } - - @Override - public int compare(CronetProvider providerLeft, CronetProvider providerRight) { - int typePreferenceLeft = evaluateCronetProviderType(providerLeft.getName()); - int typePreferenceRight = evaluateCronetProviderType(providerRight.getName()); - if (typePreferenceLeft != typePreferenceRight) { - return typePreferenceLeft - typePreferenceRight; - } - return -compareVersionStrings(providerLeft.getVersion(), providerRight.getVersion()); - } - - /** - * Convert Cronet provider name into a sortable preference value. - * Smaller values are preferred. - */ - private int evaluateCronetProviderType(String providerName) { - if (CronetProvider.PROVIDER_NAME_APP_PACKAGED.equals(providerName)) { - return 1; - } - if (gmsCoreCronetName != null && gmsCoreCronetName.equals(providerName)) { - return preferGMSCoreCronet ? 0 : 2; - } - // Unknown provider type. - return -1; - } - - /** - * Compares version strings of format "12.123.35.23". - */ - private static int compareVersionStrings(String versionLeft, String versionRight) { - if (versionLeft == null || versionRight == null) { - return 0; - } - String[] versionStringsLeft = versionLeft.split("\\."); - String[] versionStringsRight = versionRight.split("\\."); - int minLength = Math.min(versionStringsLeft.length, versionStringsRight.length); - for (int i = 0; i < minLength; i++) { - if (!versionStringsLeft[i].equals(versionStringsRight[i])) { - try { - int versionIntLeft = Integer.parseInt(versionStringsLeft[i]); - int versionIntRight = Integer.parseInt(versionStringsRight[i]); - return versionIntLeft - versionIntRight; - } catch (NumberFormatException e) { - return 0; - } - } - } - return 0; - } - } - -} 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 new file mode 100644 index 0000000000..efe30d6525 --- /dev/null +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java @@ -0,0 +1,238 @@ +/* + * 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.ext.cronet; + +import android.content.Context; +import android.support.annotation.IntDef; +import android.util.Log; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import org.chromium.net.CronetEngine; +import org.chromium.net.CronetProvider; + +/** + * A wrapper class for a {@link CronetEngine}. + */ +public final class CronetEngineWrapper { + + private static final String TAG = "CronetEngineWrapper"; + + private final CronetEngine cronetEngine; + private final @CronetEngineSource int cronetEngineSource; + + /** + * Source of {@link CronetEngine}. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({SOURCE_NATIVE, SOURCE_GMS, SOURCE_UNKNOWN, SOURCE_USER_PROVIDED, SOURCE_UNAVAILABLE}) + public @interface CronetEngineSource {} + /** + * Natively bundled Cronet implementation. + */ + public static final int SOURCE_NATIVE = 0; + /** + * Cronet implementation from GMSCore. + */ + public static final int SOURCE_GMS = 1; + /** + * Other (unknown) Cronet implementation. + */ + public static final int SOURCE_UNKNOWN = 2; + /** + * User-provided Cronet engine. + */ + public static final int SOURCE_USER_PROVIDED = 3; + /** + * No Cronet implementation available. Fallback Http provider is used if possible. + */ + public static final int SOURCE_UNAVAILABLE = 4; + + /** + * Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable + * {@link CronetProvider}. Sets wrapper to prefer natively bundled Cronet over GMSCore Cronet + * if both are available. + * + * @param context A context. + */ + public CronetEngineWrapper(Context context) { + this(context, false); + } + + /** + * Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable + * {@link CronetProvider} based on user preference. + * + * @param context A context. + * @param preferGMSCoreCronet Whether Cronet from GMSCore should be preferred over natively + * bundled Cronet if both are available. + */ + public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) { + CronetEngine cronetEngine = null; + @CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE; + List cronetProviders = CronetProvider.getAllProviders(context); + // Remove disabled and fallback Cronet providers from list + for (int i = cronetProviders.size() - 1; i >= 0; i--) { + if (!cronetProviders.get(i).isEnabled() + || CronetProvider.PROVIDER_NAME_FALLBACK.equals(cronetProviders.get(i).getName())) { + cronetProviders.remove(i); + } + } + // Sort remaining providers by type and version. + CronetProviderComparator providerComparator = new CronetProviderComparator(preferGMSCoreCronet); + Collections.sort(cronetProviders, providerComparator); + for (int i = 0; i < cronetProviders.size() && cronetEngine == null; i++) { + String providerName = cronetProviders.get(i).getName(); + try { + cronetEngine = cronetProviders.get(i).createBuilder().build(); + if (providerComparator.isNativeProvider(providerName)) { + cronetEngineSource = SOURCE_NATIVE; + } else if (providerComparator.isGMSCoreProvider(providerName)) { + cronetEngineSource = SOURCE_GMS; + } else { + cronetEngineSource = SOURCE_UNKNOWN; + } + Log.d(TAG, "CronetEngine built using " + providerName); + } catch (SecurityException e) { + Log.w(TAG, "Failed to build CronetEngine. Please check if current process has " + + "android.permission.ACCESS_NETWORK_STATE."); + } catch (UnsatisfiedLinkError e) { + Log.w(TAG, "Failed to link Cronet binaries. Please check if native Cronet binaries are " + + "bundled into your app."); + } + } + if (cronetEngine == null) { + Log.w(TAG, "Cronet not available. Using fallback provider."); + } + this.cronetEngine = cronetEngine; + this.cronetEngineSource = cronetEngineSource; + } + + /** + * Creates a wrapper for an existing CronetEngine. + * + * @param cronetEngine An existing CronetEngine. + */ + public CronetEngineWrapper(CronetEngine cronetEngine) { + this.cronetEngine = cronetEngine; + this.cronetEngineSource = SOURCE_USER_PROVIDED; + } + + /** + * Returns the source of the wrapped {@link CronetEngine}. + * + * @return A {@link CronetEngineSource} value. + */ + public @CronetEngineSource int getCronetEngineSource() { + return cronetEngineSource; + } + + /** + * Returns the wrapped {@link CronetEngine}. + * + * @return The CronetEngine, or null if no CronetEngine is available. + */ + /* package */ CronetEngine getCronetEngine() { + return cronetEngine; + } + + private static class CronetProviderComparator implements Comparator { + + private final String gmsCoreCronetName; + private final boolean preferGMSCoreCronet; + + public CronetProviderComparator(boolean preferGMSCoreCronet) { + // GMSCore CronetProvider classes are only available in some configurations. + // Thus, we use reflection to copy static name. + String gmsCoreVersionString = null; + try { + Class cronetProviderInstallerClass = + Class.forName("com.google.android.gms.net.CronetProviderInstaller"); + Field providerNameField = cronetProviderInstallerClass.getDeclaredField("PROVIDER_NAME"); + gmsCoreVersionString = (String) providerNameField.get(null); + } catch (ClassNotFoundException e) { + // GMSCore CronetProvider not available. + } catch (NoSuchFieldException e) { + // GMSCore CronetProvider not available. + } catch (IllegalAccessException e) { + // GMSCore CronetProvider not available. + } + gmsCoreCronetName = gmsCoreVersionString; + this.preferGMSCoreCronet = preferGMSCoreCronet; + } + + @Override + public int compare(CronetProvider providerLeft, CronetProvider providerRight) { + int typePreferenceLeft = evaluateCronetProviderType(providerLeft.getName()); + int typePreferenceRight = evaluateCronetProviderType(providerRight.getName()); + if (typePreferenceLeft != typePreferenceRight) { + return typePreferenceLeft - typePreferenceRight; + } + return -compareVersionStrings(providerLeft.getVersion(), providerRight.getVersion()); + } + + public boolean isNativeProvider(String providerName) { + return CronetProvider.PROVIDER_NAME_APP_PACKAGED.equals(providerName); + } + + public boolean isGMSCoreProvider(String providerName) { + return gmsCoreCronetName != null && gmsCoreCronetName.equals(providerName); + } + + /** + * Convert Cronet provider name into a sortable preference value. + * Smaller values are preferred. + */ + private int evaluateCronetProviderType(String providerName) { + if (isNativeProvider(providerName)) { + return 1; + } + if (isGMSCoreProvider(providerName)) { + return preferGMSCoreCronet ? 0 : 2; + } + // Unknown provider type. + return -1; + } + + /** + * Compares version strings of format "12.123.35.23". + */ + private static int compareVersionStrings(String versionLeft, String versionRight) { + if (versionLeft == null || versionRight == null) { + return 0; + } + String[] versionStringsLeft = versionLeft.split("\\."); + String[] versionStringsRight = versionRight.split("\\."); + int minLength = Math.min(versionStringsLeft.length, versionStringsRight.length); + for (int i = 0; i < minLength; i++) { + if (!versionStringsLeft[i].equals(versionStringsRight[i])) { + try { + int versionIntLeft = Integer.parseInt(versionStringsLeft[i]); + int versionIntRight = Integer.parseInt(versionStringsRight[i]); + return versionIntLeft - versionIntRight; + } catch (NumberFormatException e) { + return 0; + } + } + } + return 0; + } + } + +} From cdac347f8f175fb5ac5a9f4250d5462c6a083aed Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 17 May 2017 08:54:09 -0700 Subject: [PATCH 046/220] Open source IMA extension ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156312761 --- demo/build.gradle | 1 + demo/src/main/assets/media.exolist.json | 80 +++ .../exoplayer2/demo/PlayerActivity.java | 25 + .../demo/SampleChooserActivity.java | 11 +- demo/src/main/res/values/strings.xml | 2 + extensions/ima/README.md | 31 + extensions/ima/build.gradle | 35 + extensions/ima/src/main/AndroidManifest.xml | 5 + .../exoplayer2/ext/ima/AdTimeline.java | 275 ++++++++ .../exoplayer2/ext/ima/ImaAdsLoader.java | 633 ++++++++++++++++++ .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 530 +++++++++++++++ settings.gradle | 2 + 12 files changed, 1628 insertions(+), 2 deletions(-) create mode 100644 extensions/ima/README.md create mode 100644 extensions/ima/build.gradle create mode 100644 extensions/ima/src/main/AndroidManifest.xml create mode 100644 extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTimeline.java create mode 100644 extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java create mode 100644 extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java diff --git a/demo/build.gradle b/demo/build.gradle index be5e52a25c..939c5ac93d 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -52,6 +52,7 @@ dependencies { compile project(':library-ui') withExtensionsCompile project(path: ':extension-ffmpeg') withExtensionsCompile project(path: ':extension-flac') + withExtensionsCompile project(path: ':extension-ima') withExtensionsCompile project(path: ':extension-opus') withExtensionsCompile project(path: ':extension-vp9') } diff --git a/demo/src/main/assets/media.exolist.json b/demo/src/main/assets/media.exolist.json index 814c89a45b..4a51919657 100644 --- a/demo/src/main/assets/media.exolist.json +++ b/demo/src/main/assets/media.exolist.json @@ -452,5 +452,85 @@ ] } ] + }, + { + "name": "IMA sample ad tags", + "samples": [ + { + "name": "Single inline linear", + "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" + }, + { + "name": "Single skippable inline", + "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dskippablelinear&correlator=" + }, + { + "name": "Single redirect linear", + "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirectlinear&correlator=" + }, + { + "name": "Single redirect error", + "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&nofb=1&correlator=" + }, + { + "name": "Single redirect broken (fallback)", + "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&correlator=" + }, + { + "name": "VMAP pre-roll", + "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonly&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-roll + bumper", + "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonlybumper&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP post-roll", + "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonly&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP post-roll + bumper", + "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonlybumper&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-, mid- and post-rolls, single ads", + "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" + }, + { + "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad", + "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpod&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad", + "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpod&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad (bumpers around all ad breaks)", + "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpodbumper&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad (bumpers around all ad breaks)", + "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpodbumper&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad", + "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator=" + } + ] } ] diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 6cc9cabfc0..2cb35bf75e 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.demo; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; @@ -26,7 +27,9 @@ import android.text.TextUtils; import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; +import android.view.ViewGroup; import android.widget.Button; +import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; @@ -69,6 +72,7 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Util; +import java.lang.reflect.Constructor; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; @@ -92,6 +96,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay "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"; private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); private static final CookieManager DEFAULT_COOKIE_MANAGER; @@ -312,6 +317,26 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } MediaSource 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); + ViewGroup adOverlayViewGroup = new FrameLayout(this); + // Load the extension source using reflection so that demo app doesn't have to depend on it. + try { + Class clazz = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsMediaSource"); + Constructor constructor = clazz.getConstructor(MediaSource.class, + DataSource.Factory.class, Context.class, Uri.class, ViewGroup.class); + mediaSource = (MediaSource) constructor.newInstance(mediaSource, + mediaDataSourceFactory, this, adTagUri, adOverlayViewGroup); + // The demo app has a non-null overlay frame layout. + simpleExoPlayerView.getOverlayFrameLayout().addView(adOverlayViewGroup); + // Show a multi-window time bar, which will include ad break position markers. + simpleExoPlayerView.setShowMultiWindowTimeBar(true); + } catch (Exception e) { + // Throw if the media source class was not found, or there was an error instantiating it. + showToast(R.string.ima_not_loaded); + } + } boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; if (haveResumePosition) { player.seekTo(resumeWindow, resumePosition); diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 081ad190b5..87b8e92e83 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -184,6 +184,7 @@ public class SampleChooserActivity extends Activity { String[] drmKeyRequestProperties = null; boolean preferExtensionDecoders = false; ArrayList playlistSamples = null; + String adTagUri = null; reader.beginObject(); while (reader.hasNext()) { @@ -233,6 +234,9 @@ public class SampleChooserActivity extends Activity { } reader.endArray(); break; + case "ad_tag_uri": + adTagUri = reader.nextString(); + break; default: throw new ParserException("Unsupported attribute name: " + name); } @@ -246,7 +250,7 @@ public class SampleChooserActivity extends Activity { preferExtensionDecoders, playlistSamplesArray); } else { return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties, - preferExtensionDecoders, uri, extension); + preferExtensionDecoders, uri, extension, adTagUri); } } @@ -402,13 +406,15 @@ public class SampleChooserActivity extends Activity { public final String uri; public final String extension; + public final String adTagUri; public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri, - String extension) { + String extension, String adTagUri) { super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders); this.uri = uri; this.extension = extension; + this.adTagUri = adTagUri; } @Override @@ -416,6 +422,7 @@ public class SampleChooserActivity extends Activity { return super.buildIntent(context) .setData(Uri.parse(uri)) .putExtra(PlayerActivity.EXTENSION_EXTRA, extension) + .putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri) .setAction(PlayerActivity.ACTION_VIEW); } diff --git a/demo/src/main/res/values/strings.xml b/demo/src/main/res/values/strings.xml index ac17ad4443..57a05d24cd 100644 --- a/demo/src/main/res/values/strings.xml +++ b/demo/src/main/res/values/strings.xml @@ -58,4 +58,6 @@ One or more sample lists failed to load + Playing sample without ads, as the IMA extension was not loaded + diff --git a/extensions/ima/README.md b/extensions/ima/README.md new file mode 100644 index 0000000000..1dbdfd7f9b --- /dev/null +++ b/extensions/ima/README.md @@ -0,0 +1,31 @@ +# ExoPlayer IMA extension # + +## Description ## + +The IMA extension is a [MediaSource][] implementation wrapping the +[Interactive Media Ads SDK for Android][IMA]. You can use it to insert ads +alongside content. + +[IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/ +[MediaSource]: https://github.com/google/ExoPlayer/blob/release-v2/library/src/main/java/com/google/android/exoplayer2/source/MediaSource.java + +## Using the extension ## + +Pass a single-window content `MediaSource` to `ImaAdsMediaSource`'s constructor, +along with a `ViewGroup` that is on top of the player and the ad tag URI to +show. The IMA documentation includes some [sample ad tags][] for testing. Then +pass the `ImaAdsMediaSource` to `ExoPlayer.prepare`. + +[sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags + +## Known issues ## + +This is a preview version with some known issues: + +* Midroll ads are not yet fully supported. In particular, seeking with midroll +ads is not yet supported. Played ad periods are not removed. Also, `playAd` and +`AD_STARTED` events are sometimes delayed, meaning that midroll ads take a long +time to start and the ad overlay does not show immediately. +* Tapping the 'More info' button on an ad in the demo app will pause the +activity, which destroys the ImaAdsMediaSource. Played ad breaks will be +shown to the user again if the demo app returns to the foreground. diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle new file mode 100644 index 0000000000..4ba26cc244 --- /dev/null +++ b/extensions/ima/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion 14 + targetSdkVersion project.ext.targetSdkVersion + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } +} + +dependencies { + compile project(':library-core') + compile 'com.android.support:support-annotations:' + supportLibraryVersion + compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.6.0' + compile 'com.google.android.gms:play-services-ads:10.2.4' + // There exists a dependency chain: + // com.google.android.gms:play-services-ads:10.2.4 + // |-> com.google.android.gms:play-services-ads-lite:10.2.4 + // |-> com.google.android.gms:play-services-basement:10.2.4 + // |-> com.android.support:support-v4:24.0.0 + // The support-v4:24.0.0 module directly includes older versions of the same + // classes as com.android.support:support-annotations. We need to manually + // force it to the version we're using to avoid a compilation failure. This + // will become unnecessary when the support-v4 dependency in the chain above + // has been updated to 24.2.0 or later. + compile 'com.android.support:support-v4:' + supportLibraryVersion + androidTestCompile project(':library') + androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion + androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion + androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion + androidTestCompile 'com.android.support.test:runner:' + testSupportLibraryVersion +} diff --git a/extensions/ima/src/main/AndroidManifest.xml b/extensions/ima/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..22fb518c58 --- /dev/null +++ b/extensions/ima/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTimeline.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTimeline.java new file mode 100644 index 0000000000..1f8008ed10 --- /dev/null +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTimeline.java @@ -0,0 +1,275 @@ +/* + * 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.ext.ima; + +import android.util.Pair; +import com.google.ads.interactivemedia.v3.api.Ad; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; + +/** + * A {@link Timeline} for {@link ImaAdsMediaSource}. + */ +/* package */ final class AdTimeline extends Timeline { + + private static final Object AD_ID = new Object(); + + /** + * Builder for ad timelines. + */ + public static final class Builder { + + private final Timeline contentTimeline; + private final long contentDurationUs; + private final ArrayList isAd; + private final ArrayList ads; + private final ArrayList startTimesUs; + private final ArrayList endTimesUs; + private final ArrayList uids; + + /** + * Creates a new ad timeline builder using the specified {@code contentTimeline} as the timeline + * of the content within which to insert ad breaks. + * + * @param contentTimeline The timeline of the content within which to insert ad breaks. + */ + public Builder(Timeline contentTimeline) { + this.contentTimeline = contentTimeline; + contentDurationUs = contentTimeline.getPeriod(0, new Period()).durationUs; + isAd = new ArrayList<>(); + ads = new ArrayList<>(); + startTimesUs = new ArrayList<>(); + endTimesUs = new ArrayList<>(); + uids = new ArrayList<>(); + } + + /** + * Adds an ad period. Each individual ad in an ad pod is represented by a separate ad period. + * + * @param ad The {@link Ad} instance representing the ad break, or {@code null} if not known. + * @param adBreakIndex The index of the ad break that contains the ad in the timeline. + * @param adIndexInAdBreak The index of the ad in its ad break. + * @param durationUs The duration of the ad, in microseconds. May be {@link C#TIME_UNSET}. + * @return The builder. + */ + public Builder addAdPeriod(Ad ad, int adBreakIndex, int adIndexInAdBreak, long durationUs) { + isAd.add(true); + ads.add(ad); + startTimesUs.add(0L); + endTimesUs.add(durationUs); + uids.add(Pair.create(adBreakIndex, adIndexInAdBreak)); + return this; + } + + /** + * Adds a content period. + * + * @param startTimeUs The start time of the period relative to the start of the content + * timeline, in microseconds. + * @param endTimeUs The end time of the period relative to the start of the content timeline, in + * microseconds. May be {@link C#TIME_UNSET} to include the rest of the content. + * @return The builder. + */ + public Builder addContent(long startTimeUs, long endTimeUs) { + ads.add(null); + isAd.add(false); + startTimesUs.add(startTimeUs); + endTimesUs.add(endTimeUs == C.TIME_UNSET ? contentDurationUs : endTimeUs); + uids.add(Pair.create(startTimeUs, endTimeUs)); + return this; + } + + /** + * Builds and returns the ad timeline. + */ + public AdTimeline build() { + int periodCount = uids.size(); + Assertions.checkState(periodCount > 0); + Ad[] ads = new Ad[periodCount]; + boolean[] isAd = new boolean[periodCount]; + long[] startTimesUs = new long[periodCount]; + long[] endTimesUs = new long[periodCount]; + for (int i = 0; i < periodCount; i++) { + ads[i] = this.ads.get(i); + isAd[i] = this.isAd.get(i); + startTimesUs[i] = this.startTimesUs.get(i); + endTimesUs[i] = this.endTimesUs.get(i); + } + Object[] uids = this.uids.toArray(new Object[periodCount]); + return new AdTimeline(contentTimeline, isAd, ads, startTimesUs, endTimesUs, uids); + } + + } + + private final Period contentPeriod; + private final Window contentWindow; + private final boolean[] isAd; + private final Ad[] ads; + private final long[] startTimesUs; + private final long[] endTimesUs; + private final Object[] uids; + + private AdTimeline(Timeline contentTimeline, boolean[] isAd, Ad[] ads, long[] startTimesUs, + long[] endTimesUs, Object[] uids) { + contentWindow = contentTimeline.getWindow(0, new Window(), true); + contentPeriod = contentTimeline.getPeriod(0, new Period(), true); + this.isAd = isAd; + this.ads = ads; + this.startTimesUs = startTimesUs; + this.endTimesUs = endTimesUs; + this.uids = uids; + } + + /** + * Returns whether the period at {@code index} contains ad media. + */ + public boolean isPeriodAd(int index) { + return isAd[index]; + } + + /** + * Returns the duration of the content within which ads have been inserted, in microseconds. + */ + public long getContentDurationUs() { + return contentPeriod.durationUs; + } + + /** + * Returns the start time of the period at {@code periodIndex} relative to the start of the + * content, in microseconds. + * + * @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not a content + * period. + */ + public long getContentStartTimeUs(int periodIndex) { + Assertions.checkArgument(!isAd[periodIndex]); + return startTimesUs[periodIndex]; + } + + /** + * Returns the end time of the period at {@code periodIndex} relative to the start of the content, + * in microseconds. + * + * @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not a content + * period. + */ + public long getContentEndTimeUs(int periodIndex) { + Assertions.checkArgument(!isAd[periodIndex]); + return endTimesUs[periodIndex]; + } + + /** + * Returns the index of the ad break to which the period at {@code periodIndex} belongs. + * + * @param periodIndex The period index. + * @return The index of the ad break to which the period belongs. + * @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not an ad. + */ + public int getAdBreakIndex(int periodIndex) { + Assertions.checkArgument(isAd[periodIndex]); + int adBreakIndex = 0; + for (int i = 1; i < periodIndex; i++) { + if (!isAd[i] && isAd[i - 1]) { + adBreakIndex++; + } + } + return adBreakIndex; + } + + /** + * Returns the index of the ad at {@code periodIndex} in its ad break. + * + * @param periodIndex The period index. + * @return The index of the ad at {@code periodIndex} in its ad break. + * @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not an ad. + */ + public int getAdIndexInAdBreak(int periodIndex) { + Assertions.checkArgument(isAd[periodIndex]); + int adIndex = 0; + for (int i = 0; i < periodIndex; i++) { + if (isAd[i]) { + adIndex++; + } else { + adIndex = 0; + } + } + return adIndex; + } + + @Override + public int getWindowCount() { + return uids.length; + } + + @Override + public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + if (repeatMode == ExoPlayer.REPEAT_MODE_ONE) { + repeatMode = ExoPlayer.REPEAT_MODE_ALL; + } + return super.getNextWindowIndex(windowIndex, repeatMode); + } + + @Override + public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + if (repeatMode == ExoPlayer.REPEAT_MODE_ONE) { + repeatMode = ExoPlayer.REPEAT_MODE_ALL; + } + return super.getPreviousWindowIndex(windowIndex, repeatMode); + } + + @Override + public Window getWindow(int index, Window window, boolean setIds, + long defaultPositionProjectionUs) { + long startTimeUs = startTimesUs[index]; + long durationUs = endTimesUs[index] - startTimeUs; + if (isAd[index]) { + window.set(ads[index], C.TIME_UNSET, C.TIME_UNSET, false, false, 0L, durationUs, index, index, + 0L); + } else { + window.set(contentWindow.id, contentWindow.presentationStartTimeMs + C.usToMs(startTimeUs), + contentWindow.windowStartTimeMs + C.usToMs(startTimeUs), contentWindow.isSeekable, false, + 0L, durationUs, index, index, 0L); + } + return window; + } + + @Override + public int getPeriodCount() { + return uids.length; + } + + @Override + public Period getPeriod(int index, Period period, boolean setIds) { + Object id = setIds ? (isAd[index] ? AD_ID : contentPeriod.id) : null; + return period.set(id, uids[index], index, endTimesUs[index] - startTimesUs[index], 0, + isAd[index]); + } + + @Override + public int getIndexOfPeriod(Object uid) { + for (int i = 0; i < uids.length; i++) { + if (Util.areEqual(uid, uids[i])) { + return i; + } + } + return C.INDEX_UNSET; + } + +} 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 new file mode 100644 index 0000000000..0c89ff604c --- /dev/null +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -0,0 +1,633 @@ +/* + * 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.ext.ima; + +import android.content.Context; +import android.net.Uri; +import android.os.SystemClock; +import android.util.Log; +import android.view.ViewGroup; +import com.google.ads.interactivemedia.v3.api.Ad; +import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; +import com.google.ads.interactivemedia.v3.api.AdErrorEvent; +import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; +import com.google.ads.interactivemedia.v3.api.AdEvent; +import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener; +import com.google.ads.interactivemedia.v3.api.AdPodInfo; +import com.google.ads.interactivemedia.v3.api.AdsLoader; +import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener; +import com.google.ads.interactivemedia.v3.api.AdsManager; +import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; +import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; +import com.google.ads.interactivemedia.v3.api.AdsRequest; +import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; +import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; +import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; +import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; +import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Loads ads using the IMA SDK. All methods are called on the main thread. + */ +/* package */ final class ImaAdsLoader implements ExoPlayer.EventListener, VideoAdPlayer, + ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener { + + private static final boolean DEBUG = false; + private static final String TAG = "ImaAdsLoader"; + + /** + * Listener for ad loader events. All methods are called on the main thread. + */ + public interface EventListener { + + /** + * Called when the timestamps of ad breaks are known. + * + * @param adBreakTimesUs The times of ad breaks, in microseconds. + */ + void onAdBreakTimesUsLoaded(long[] adBreakTimesUs); + + /** + * Called when the URI for the media of an ad has been loaded. + * + * @param adBreakIndex The index of the ad break containing the ad with the media URI. + * @param adIndexInAdBreak The index of the ad in its ad break. + * @param uri The URI for the ad's media. + */ + void onUriLoaded(int adBreakIndex, int adIndexInAdBreak, Uri uri); + + /** + * Called when the {@link Ad} instance for a specified ad has been loaded. + * + * @param adBreakIndex The index of the ad break containing the ad. + * @param adIndexInAdBreak The index of the ad in its ad break. + * @param ad The {@link Ad} instance for the ad. + */ + void onAdLoaded(int adBreakIndex, int adIndexInAdBreak, Ad ad); + + /** + * Called when the specified ad break has been played to the end. + * + * @param adBreakIndex The index of the ad break. + */ + void onAdBreakPlayedToEnd(int adBreakIndex); + + /** + * Called when there was an error loading ads. + * + * @param error The error. + */ + void onLoadError(IOException error); + + } + + /** + * Whether to enable preloading of ads in {@link AdsRenderingSettings}. + */ + private static final boolean ENABLE_PRELOADING = true; + + private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = + "google/com.google.android.exoplayer2.ext.ima"; + private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; + + /** + * Threshold before the end of content at which IMA is notified that content is complete if the + * player buffers, in milliseconds. + */ + private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000; + + private final EventListener eventListener; + private final ExoPlayer player; + private final Timeline.Period period; + private final List adCallbacks; + private final AdsLoader adsLoader; + + private AdsManager adsManager; + private AdTimeline adTimeline; + private long contentDurationMs; + private int lastContentPeriodIndex; + + private int playerPeriodIndex; + + private boolean released; + + // Fields tracking IMA's state. + + /** + * The index of the current ad break that IMA is loading. + */ + private int adBreakIndex; + /** + * The index of the ad within its ad break, in {@link #loadAd(String)}. + */ + private int adIndexInAdBreak; + /** + * The total number of ads in the current ad break, or {@link C#INDEX_UNSET} if unknown. + */ + private int adCountInAdBreak; + + /** + * Tracks the period currently being played in IMA's model of playback. + */ + private int imaPeriodIndex; + /** + * Whether the period at {@link #imaPeriodIndex} is an ad. + */ + private boolean isAdDisplayed; + /** + * Whether {@link AdsLoader#contentComplete()} has been called since starting ad playback. + */ + private boolean sentContentComplete; + /** + * If {@link #isAdDisplayed} is set, stores whether IMA has called {@link #playAd()} and not + * {@link #stopAd()}. + */ + private boolean playingAd; + /** + * If {@link #isAdDisplayed} is set, stores whether IMA has called {@link #pauseAd()} since a + * preceding call to {@link #playAd()} for the current ad. + */ + private boolean pausedInAd; + /** + * If a content period has finished but IMA has not yet sent an ad event with + * {@link AdEvent.AdEventType#CONTENT_PAUSE_REQUESTED}, stores the value of + * {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to + * determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise. + */ + private long fakeContentProgressElapsedRealtimeMs; + + /** + * Creates a new IMA ads loader. + * + * @param context The context. + * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See + * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for + * more information. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to + * use the default settings. If set, the player type and version fields may be overwritten. + * @param player The player instance that will play the loaded ad schedule. The player's timeline + * must be an {@link AdTimeline} matching the loaded ad schedule. + * @param eventListener Listener for ad loader events. + */ + public ImaAdsLoader(Context context, Uri adTagUri, ViewGroup adUiViewGroup, + ImaSdkSettings imaSdkSettings, ExoPlayer player, EventListener eventListener) { + this.eventListener = eventListener; + this.player = player; + period = new Timeline.Period(); + adCallbacks = new ArrayList<>(1); + + lastContentPeriodIndex = C.INDEX_UNSET; + adCountInAdBreak = C.INDEX_UNSET; + fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; + + player.addListener(this); + + ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); + AdDisplayContainer adDisplayContainer = imaSdkFactory.createAdDisplayContainer(); + adDisplayContainer.setPlayer(this); + adDisplayContainer.setAdContainer(adUiViewGroup); + + if (imaSdkSettings == null) { + imaSdkSettings = imaSdkFactory.createImaSdkSettings(); + } + imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); + imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); + + AdsRequest request = imaSdkFactory.createAdsRequest(); + request.setAdTagUrl(adTagUri.toString()); + request.setAdDisplayContainer(adDisplayContainer); + request.setContentProgressProvider(this); + + adsLoader = imaSdkFactory.createAdsLoader(context, imaSdkSettings); + adsLoader.addAdErrorListener(this); + adsLoader.addAdsLoadedListener(this); + adsLoader.requestAds(request); + } + + /** + * Releases the loader. Must be called when the instance is no longer needed. + */ + public void release() { + if (adsManager != null) { + adsManager.destroy(); + adsManager = null; + } + player.removeListener(this); + released = true; + } + + // AdsLoader.AdsLoadedListener implementation. + + @Override + public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { + adsManager = adsManagerLoadedEvent.getAdsManager(); + adsManager.addAdErrorListener(this); + adsManager.addAdEventListener(this); + if (ENABLE_PRELOADING) { + ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); + AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); + adsRenderingSettings.setEnablePreloading(true); + adsManager.init(adsRenderingSettings); + if (DEBUG) { + Log.d(TAG, "Initialized with preloading"); + } + } else { + adsManager.init(); + if (DEBUG) { + Log.d(TAG, "Initialized without preloading"); + } + } + eventListener.onAdBreakTimesUsLoaded(getAdBreakTimesUs(adsManager.getAdCuePoints())); + } + + // AdEvent.AdEventListener implementation. + + @Override + public void onAdEvent(AdEvent adEvent) { + if (DEBUG) { + Log.d(TAG, "onAdEvent " + adEvent.getType()); + } + if (released) { + // The ads manager may pass CONTENT_RESUME_REQUESTED after it is destroyed. + return; + } + switch (adEvent.getType()) { + case LOADED: + adsManager.start(); + break; + case STARTED: + // Note: This event is sometimes delivered several seconds after playAd is called. + // See [Internal: b/37775441]. + Ad ad = adEvent.getAd(); + AdPodInfo adPodInfo = ad.getAdPodInfo(); + adCountInAdBreak = adPodInfo.getTotalAds(); + int adPosition = adPodInfo.getAdPosition(); + eventListener.onAdLoaded(adBreakIndex, adPosition - 1, ad); + if (DEBUG) { + Log.d(TAG, "Started ad " + adPosition + " of " + adCountInAdBreak + " in ad break " + + adBreakIndex); + } + break; + case CONTENT_PAUSE_REQUESTED: + // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads + // before sending CONTENT_RESUME_REQUESTED. + pauseContentInternal(); + break; + case SKIPPED: // Fall through. + case CONTENT_RESUME_REQUESTED: + resumeContentInternal(); + break; + case ALL_ADS_COMPLETED: + // Do nothing. The ads manager will be released when the source is released. + default: + break; + } + } + + // AdErrorEvent.AdErrorListener implementation. + + @Override + public void onAdError(AdErrorEvent adErrorEvent) { + if (DEBUG) { + Log.d(TAG, "onAdError " + adErrorEvent); + } + IOException exception = new IOException("Ad error: " + adErrorEvent, adErrorEvent.getError()); + eventListener.onLoadError(exception); + // TODO: Provide a timeline to the player if it doesn't have one yet, so the content can play. + } + + // ContentProgressProvider implementation. + + @Override + public VideoProgressUpdate getContentProgress() { + if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { + long contentEndTimeMs = C.usToMs(adTimeline.getContentEndTimeUs(imaPeriodIndex)); + long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; + return new VideoProgressUpdate(contentEndTimeMs + elapsedSinceEndMs, contentDurationMs); + } + + if (adTimeline == null || isAdDisplayed || imaPeriodIndex != playerPeriodIndex + || contentDurationMs == C.TIME_UNSET) { + return VideoProgressUpdate.VIDEO_TIME_NOT_READY; + } + checkForContentComplete(); + long positionMs = C.usToMs(adTimeline.getContentStartTimeUs(imaPeriodIndex)) + + player.getCurrentPosition(); + return new VideoProgressUpdate(positionMs, contentDurationMs); + } + + // VideoAdPlayer implementation. + + @Override + public VideoProgressUpdate getAdProgress() { + if (adTimeline == null || !isAdDisplayed || imaPeriodIndex != playerPeriodIndex + || adTimeline.getPeriod(imaPeriodIndex, period).getDurationUs() == C.TIME_UNSET) { + return VideoProgressUpdate.VIDEO_TIME_NOT_READY; + } + return new VideoProgressUpdate(player.getCurrentPosition(), period.getDurationMs()); + } + + @Override + public void loadAd(String adUriString) { + if (DEBUG) { + Log.d(TAG, "loadAd at index " + adIndexInAdBreak + " in ad break " + adBreakIndex); + } + eventListener.onUriLoaded(adBreakIndex, adIndexInAdBreak, Uri.parse(adUriString)); + adIndexInAdBreak++; + } + + @Override + public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) { + adCallbacks.add(videoAdPlayerCallback); + } + + @Override + public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) { + adCallbacks.remove(videoAdPlayerCallback); + } + + @Override + public void playAd() { + if (DEBUG) { + Log.d(TAG, "playAd"); + } + Assertions.checkState(isAdDisplayed); + if (playingAd && !pausedInAd) { + // Work around an issue where IMA does not always call stopAd before resuming content. + // See [Internal: b/38354028]. + if (DEBUG) { + Log.d(TAG, "Unexpected playAd without stopAd"); + } + stopAdInternal(); + } + player.setPlayWhenReady(true); + if (!playingAd) { + playingAd = true; + for (VideoAdPlayerCallback callback : adCallbacks) { + callback.onPlay(); + } + } else if (pausedInAd) { + pausedInAd = false; + for (VideoAdPlayerCallback callback : adCallbacks) { + callback.onResume(); + } + } + } + + @Override + public void stopAd() { + if (!playingAd) { + if (DEBUG) { + Log.d(TAG, "Ignoring unexpected stopAd"); + } + return; + } + if (DEBUG) { + Log.d(TAG, "stopAd"); + } + stopAdInternal(); + } + + @Override + public void pauseAd() { + if (DEBUG) { + Log.d(TAG, "pauseAd"); + } + if (released || !playingAd) { + // This method is called after content is resumed, and may also be called after release. + return; + } + pausedInAd = true; + player.setPlayWhenReady(false); + for (VideoAdPlayerCallback callback : adCallbacks) { + callback.onPause(); + } + } + + @Override + public void resumeAd() { + // This method is never called. See [Internal: b/18931719]. + throw new IllegalStateException(); + } + + // ExoPlayer.EventListener implementation. + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + if (timeline.isEmpty()) { + // The player is being re-prepared and this source will be released. + return; + } + if (adTimeline == null) { + // TODO: Handle initial seeks after the first period. + isAdDisplayed = timeline.getPeriod(0, period).isAd; + imaPeriodIndex = 0; + player.seekTo(0, 0); + } + adTimeline = (AdTimeline) timeline; + contentDurationMs = C.usToMs(adTimeline.getContentDurationUs()); + lastContentPeriodIndex = adTimeline.getPeriodCount() - 1; + while (adTimeline.isPeriodAd(lastContentPeriodIndex)) { + // All timelines have at least one content period. + lastContentPeriodIndex--; + } + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + // Do nothing. + } + + @Override + public void onLoadingChanged(boolean isLoading) { + // Do nothing. + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (playbackState == ExoPlayer.STATE_BUFFERING && playWhenReady) { + checkForContentComplete(); + } else if (playbackState == ExoPlayer.STATE_ENDED && isAdDisplayed) { + // IMA is waiting for the ad playback to finish so invoke the callback now. + // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. + for (VideoAdPlayerCallback callback : adCallbacks) { + callback.onEnded(); + } + } + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + if (isAdDisplayed && adTimeline.isPeriodAd(playerPeriodIndex)) { + for (VideoAdPlayerCallback callback : adCallbacks) { + callback.onError(); + } + } + } + + @Override + public void onPositionDiscontinuity() { + if (player.getCurrentPeriodIndex() == playerPeriodIndex + 1) { + if (isAdDisplayed) { + // IMA is waiting for the ad playback to finish so invoke the callback now. + // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. + for (VideoAdPlayerCallback callback : adCallbacks) { + callback.onEnded(); + } + } else { + player.setPlayWhenReady(false); + if (imaPeriodIndex == playerPeriodIndex) { + // IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position. + Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET); + fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); + } + } + } + playerPeriodIndex = player.getCurrentPeriodIndex(); + } + + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // Do nothing. + } + + // Internal methods. + + /** + * Resumes the player, ensuring the current period is a content period by seeking if necessary. + */ + private void resumeContentInternal() { + if (adTimeline != null) { + if (imaPeriodIndex < lastContentPeriodIndex) { + if (playingAd) { + // Work around an issue where IMA does not always call stopAd before resuming content. + // See [Internal: b/38354028]. + if (DEBUG) { + Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd"); + } + stopAdInternal(); + } + while (adTimeline.isPeriodAd(imaPeriodIndex)) { + imaPeriodIndex++; + } + synchronizePlayerToIma(); + } + } + player.setPlayWhenReady(true); + } + + /** + * Pauses the player, and ensures that the current period is an ad period by seeking if necessary. + */ + private void pauseContentInternal() { + // IMA is requesting to pause content, so stop faking the content position. + fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; + if (adTimeline != null && !isAdDisplayed) { + // Seek to the next ad. + while (!adTimeline.isPeriodAd(imaPeriodIndex)) { + imaPeriodIndex++; + } + synchronizePlayerToIma(); + } else { + // IMA is sending an initial CONTENT_PAUSE_REQUESTED before a pre-roll ad. + Assertions.checkState(playerPeriodIndex == 0 && imaPeriodIndex == 0); + } + player.setPlayWhenReady(false); + } + + /** + * Stops the currently playing ad, seeking to the next content period if there is one. May only be + * called when {@link #playingAd} is {@code true}. + */ + private void stopAdInternal() { + Assertions.checkState(playingAd); + if (imaPeriodIndex != adTimeline.getPeriodCount() - 1) { + player.setPlayWhenReady(false); + imaPeriodIndex++; + if (!adTimeline.isPeriodAd(imaPeriodIndex)) { + eventListener.onAdBreakPlayedToEnd(adBreakIndex); + adBreakIndex++; + adIndexInAdBreak = 0; + } + synchronizePlayerToIma(); + } else { + eventListener.onAdBreakPlayedToEnd(adTimeline.getAdBreakIndex(imaPeriodIndex)); + } + } + + private void synchronizePlayerToIma() { + if (playerPeriodIndex != imaPeriodIndex) { + player.seekTo(imaPeriodIndex, 0); + } + + isAdDisplayed = adTimeline.isPeriodAd(imaPeriodIndex); + // If an ad is displayed, these flags will be updated in response to playAd/pauseAd/stopAd until + // the content is resumed. + playingAd = false; + pausedInAd = false; + } + + private void checkForContentComplete() { + if (adTimeline == null || isAdDisplayed || sentContentComplete) { + return; + } + long positionMs = C.usToMs(adTimeline.getContentStartTimeUs(imaPeriodIndex)) + + player.getCurrentPosition(); + if (playerPeriodIndex == lastContentPeriodIndex + && positionMs + END_OF_CONTENT_POSITION_THRESHOLD_MS + >= C.usToMs(adTimeline.getContentEndTimeUs(playerPeriodIndex))) { + adsLoader.contentComplete(); + if (DEBUG) { + Log.d(TAG, "adsLoader.contentComplete"); + } + sentContentComplete = true; + } + } + + private static long[] getAdBreakTimesUs(List cuePoints) { + if (cuePoints.isEmpty()) { + // If no cue points are specified, there is a preroll ad break. + return new long[] {0}; + } + + int count = cuePoints.size(); + long[] adBreakTimesUs = new long[count]; + for (int i = 0; i < count; i++) { + double cuePoint = cuePoints.get(i); + adBreakTimesUs[i] = cuePoint == -1.0 ? C.TIME_UNSET : (long) (C.MICROS_PER_SECOND * cuePoint); + } + return adBreakTimesUs; + } + +} diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java new file mode 100644 index 0000000000..0055fbca32 --- /dev/null +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -0,0 +1,530 @@ +/* + * 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.ext.ima; + +import android.content.Context; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.view.ViewGroup; +import com.google.ads.interactivemedia.v3.api.Ad; +import com.google.ads.interactivemedia.v3.api.AdPodInfo; +import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.source.ClippingMediaPeriod; +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * A {@link MediaSource} that inserts ads linearly with a provided content media source using the + * Interactive Media Ads SDK for ad loading and tracking. + */ +public final class ImaAdsMediaSource implements MediaSource { + + private final MediaSource contentMediaSource; + private final DataSource.Factory dataSourceFactory; + private final Context context; + private final Uri adTagUri; + private final ViewGroup adUiViewGroup; + private final ImaSdkSettings imaSdkSettings; + private final Handler mainHandler; + private final AdListener adLoaderListener; + private final Map mediaSourceByMediaPeriod; + + private Handler playerHandler; + private ExoPlayer player; + private volatile boolean released; + + // Accessed on the player thread. + private Timeline contentTimeline; + private Object contentManifest; + private long[] adBreakTimesUs; + private boolean[] playedAdBreak; + private Ad[][] adBreakAds; + private Timeline[][] adBreakTimelines; + private MediaSource[][] adBreakMediaSources; + private DeferredMediaPeriod[][] adBreakDeferredMediaPeriods; + private AdTimeline timeline; + private MediaSource.Listener listener; + private IOException adLoadError; + + // Accessed on the main thread. + private ImaAdsLoader imaAdsLoader; + + /** + * Constructs a new source that inserts ads linearly with the content specified by + * {@code contentMediaSource}. + * + * @param contentMediaSource The {@link MediaSource} providing the content to play. + * @param dataSourceFactory Factory for data sources used to load ad media. + * @param context The context. + * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See + * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for + * more information. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad user + * interface. + */ + public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, + Context context, Uri adTagUri, ViewGroup adUiViewGroup) { + this(contentMediaSource, dataSourceFactory, context, adTagUri, adUiViewGroup, null); + } + + /** + * Constructs a new source that inserts ads linearly with the content specified by + * {@code contentMediaSource}. + * + * @param contentMediaSource The {@link MediaSource} providing the content to play. + * @param dataSourceFactory Factory for data sources used to load ad media. + * @param context The context. + * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See + * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for + * more information. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to + * use the default settings. If set, the player type and version fields may be overwritten. + */ + public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, + Context context, Uri adTagUri, ViewGroup adUiViewGroup, ImaSdkSettings imaSdkSettings) { + this.contentMediaSource = contentMediaSource; + this.dataSourceFactory = dataSourceFactory; + this.context = context; + this.adTagUri = adTagUri; + this.adUiViewGroup = adUiViewGroup; + this.imaSdkSettings = imaSdkSettings; + mainHandler = new Handler(Looper.getMainLooper()); + adLoaderListener = new AdListener(); + mediaSourceByMediaPeriod = new HashMap<>(); + adBreakMediaSources = new MediaSource[0][]; + } + + @Override + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkArgument(isTopLevelSource); + this.listener = listener; + this.player = player; + playerHandler = new Handler(); + mainHandler.post(new Runnable() { + @Override + public void run() { + imaAdsLoader = new ImaAdsLoader(context, adTagUri, adUiViewGroup, imaSdkSettings, + ImaAdsMediaSource.this.player, adLoaderListener); + } + }); + contentMediaSource.prepareSource(player, false, new Listener() { + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + ImaAdsMediaSource.this.onContentSourceInfoRefreshed(timeline, manifest); + } + }); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + if (adLoadError != null) { + throw adLoadError; + } + contentMediaSource.maybeThrowSourceInfoRefreshError(); + for (MediaSource[] mediaSources : adBreakMediaSources) { + for (MediaSource mediaSource : mediaSources) { + mediaSource.maybeThrowSourceInfoRefreshError(); + } + } + } + + @Override + public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + if (timeline.isPeriodAd(index)) { + int adBreakIndex = timeline.getAdBreakIndex(index); + int adIndexInAdBreak = timeline.getAdIndexInAdBreak(index); + if (adIndexInAdBreak >= adBreakMediaSources[adBreakIndex].length) { + DeferredMediaPeriod deferredPeriod = new DeferredMediaPeriod(0, allocator, positionUs); + if (adIndexInAdBreak >= adBreakDeferredMediaPeriods[adBreakIndex].length) { + adBreakDeferredMediaPeriods[adBreakIndex] = Arrays.copyOf( + adBreakDeferredMediaPeriods[adBreakIndex], adIndexInAdBreak + 1); + } + adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak] = deferredPeriod; + return deferredPeriod; + } + + MediaSource adBreakMediaSource = adBreakMediaSources[adBreakIndex][adIndexInAdBreak]; + MediaPeriod adBreakMediaPeriod = adBreakMediaSource.createPeriod(0, allocator, positionUs); + mediaSourceByMediaPeriod.put(adBreakMediaPeriod, adBreakMediaSource); + return adBreakMediaPeriod; + } else { + long startUs = timeline.getContentStartTimeUs(index); + long endUs = timeline.getContentEndTimeUs(index); + long contentStartUs = startUs + positionUs; + MediaPeriod contentMediaPeriod = contentMediaSource.createPeriod(0, allocator, + contentStartUs); + ClippingMediaPeriod clippingPeriod = new ClippingMediaPeriod(contentMediaPeriod); + clippingPeriod.setClipping(startUs, endUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE : endUs); + mediaSourceByMediaPeriod.put(contentMediaPeriod, contentMediaSource); + return clippingPeriod; + } + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + if (mediaPeriod instanceof DeferredMediaPeriod) { + mediaPeriod = ((DeferredMediaPeriod) mediaPeriod).mediaPeriod; + if (mediaPeriod == null) { + // Nothing to do. + return; + } + } else if (mediaPeriod instanceof ClippingMediaPeriod) { + mediaPeriod = ((ClippingMediaPeriod) mediaPeriod).mediaPeriod; + } + mediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod); + } + + @Override + public void releaseSource() { + released = true; + adLoadError = null; + contentMediaSource.releaseSource(); + for (MediaSource[] mediaSources : adBreakMediaSources) { + for (MediaSource mediaSource : mediaSources) { + mediaSource.releaseSource(); + } + } + mainHandler.post(new Runnable() { + @Override + public void run() { + // TODO: The source will be released when the application is paused/stopped, which can occur + // if the user taps on the ad. In this case, we should keep the ads manager alive but pause + // it, instead of destroying it. + imaAdsLoader.release(); + imaAdsLoader = null; + } + }); + } + + // Internal methods. + + private void onAdBreakTimesUsLoaded(long[] adBreakTimesUs) { + Assertions.checkState(this.adBreakTimesUs == null); + this.adBreakTimesUs = adBreakTimesUs; + int adBreakCount = adBreakTimesUs.length; + adBreakAds = new Ad[adBreakCount][]; + Arrays.fill(adBreakAds, new Ad[0]); + adBreakTimelines = new Timeline[adBreakCount][]; + Arrays.fill(adBreakTimelines, new Timeline[0]); + adBreakMediaSources = new MediaSource[adBreakCount][]; + Arrays.fill(adBreakMediaSources, new MediaSource[0]); + adBreakDeferredMediaPeriods = new DeferredMediaPeriod[adBreakCount][]; + Arrays.fill(adBreakDeferredMediaPeriods, new DeferredMediaPeriod[0]); + playedAdBreak = new boolean[adBreakCount]; + maybeUpdateSourceInfo(); + } + + private void onContentSourceInfoRefreshed(Timeline timeline, Object manifest) { + contentTimeline = timeline; + contentManifest = manifest; + maybeUpdateSourceInfo(); + } + + private void onAdUriLoaded(final int adBreakIndex, final int adIndexInAdBreak, Uri uri) { + MediaSource adMediaSource = new ExtractorMediaSource(uri, dataSourceFactory, + new DefaultExtractorsFactory(), mainHandler, adLoaderListener); + if (adBreakMediaSources[adBreakIndex].length <= adIndexInAdBreak) { + int adCount = adIndexInAdBreak + 1; + adBreakMediaSources[adBreakIndex] = Arrays.copyOf(adBreakMediaSources[adBreakIndex], adCount); + adBreakTimelines[adBreakIndex] = Arrays.copyOf(adBreakTimelines[adBreakIndex], adCount); + } + adBreakMediaSources[adBreakIndex][adIndexInAdBreak] = adMediaSource; + if (adIndexInAdBreak < adBreakDeferredMediaPeriods[adBreakIndex].length + && adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak] != null) { + adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak].setMediaSource( + adBreakMediaSources[adBreakIndex][adIndexInAdBreak]); + mediaSourceByMediaPeriod.put( + adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak].mediaPeriod, adMediaSource); + } + adMediaSource.prepareSource(player, false, new Listener() { + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + onAdSourceInfoRefreshed(adBreakIndex, adIndexInAdBreak, timeline); + } + }); + } + + private void onAdSourceInfoRefreshed(int adBreakIndex, int adIndexInAdBreak, Timeline timeline) { + adBreakTimelines[adBreakIndex][adIndexInAdBreak] = timeline; + maybeUpdateSourceInfo(); + } + + private void onAdLoaded(int adBreakIndex, int adIndexInAdBreak, Ad ad) { + if (adBreakAds[adBreakIndex].length <= adIndexInAdBreak) { + int adCount = adIndexInAdBreak + 1; + adBreakAds[adBreakIndex] = Arrays.copyOf(adBreakAds[adBreakIndex], adCount); + } + adBreakAds[adBreakIndex][adIndexInAdBreak] = ad; + maybeUpdateSourceInfo(); + } + + private void maybeUpdateSourceInfo() { + if (adBreakTimesUs == null || contentTimeline == null) { + // We don't have enough information to start building the timeline yet. + return; + } + + AdTimeline.Builder builder = new AdTimeline.Builder(contentTimeline); + int count = adBreakTimesUs.length; + boolean preroll = adBreakTimesUs[0] == 0; + boolean postroll = adBreakTimesUs[count - 1] == C.TIME_UNSET; + int midrollCount = count - (preroll ? 1 : 0) - (postroll ? 1 : 0); + + int adBreakIndex = 0; + long contentTimeUs = 0; + if (preroll) { + addAdBreak(builder, adBreakIndex++); + } + for (int i = 0; i < midrollCount; i++) { + long startTimeUs = contentTimeUs; + contentTimeUs = adBreakTimesUs[adBreakIndex]; + builder.addContent(startTimeUs, contentTimeUs); + addAdBreak(builder, adBreakIndex++); + } + builder.addContent(contentTimeUs, C.TIME_UNSET); + if (postroll) { + addAdBreak(builder, adBreakIndex); + } + + timeline = builder.build(); + listener.onSourceInfoRefreshed(timeline, contentManifest); + } + + private void addAdBreak(AdTimeline.Builder builder, int adBreakIndex) { + int adCount = adBreakMediaSources[adBreakIndex].length; + AdPodInfo adPodInfo = null; + for (int adIndex = 0; adIndex < adCount; adIndex++) { + Timeline adTimeline = adBreakTimelines[adBreakIndex][adIndex]; + long adDurationUs = adTimeline != null + ? adTimeline.getPeriod(0, new Timeline.Period()).getDurationUs() : C.TIME_UNSET; + Ad ad = adIndex < adBreakAds[adBreakIndex].length + ? adBreakAds[adBreakIndex][adIndex] : null; + builder.addAdPeriod(ad, adBreakIndex, adIndex, adDurationUs); + if (ad != null) { + adPodInfo = ad.getAdPodInfo(); + } + } + if (adPodInfo == null || adPodInfo.getTotalAds() > adCount) { + // We don't know how many ads are in the ad break, or they have not loaded yet. + builder.addAdPeriod(null, adBreakIndex, adCount, C.TIME_UNSET); + } + } + + private void onAdBreakPlayedToEnd(int adBreakIndex) { + playedAdBreak[adBreakIndex] = true; + } + + /** + * Listener for ad loading events. All methods are called on the main thread. + */ + private final class AdListener implements ImaAdsLoader.EventListener, + ExtractorMediaSource.EventListener { + + @Override + public void onAdBreakTimesUsLoaded(final long[] adBreakTimesUs) { + if (released) { + return; + } + playerHandler.post(new Runnable() { + @Override + public void run() { + if (released) { + return; + } + ImaAdsMediaSource.this.onAdBreakTimesUsLoaded(adBreakTimesUs); + } + }); + } + + @Override + public void onUriLoaded(final int adBreakIndex, final int adIndexInAdBreak, final Uri uri) { + if (released) { + return; + } + playerHandler.post(new Runnable() { + @Override + public void run() { + if (released) { + return; + } + ImaAdsMediaSource.this.onAdUriLoaded(adBreakIndex, adIndexInAdBreak, uri); + } + }); + } + + @Override + public void onAdLoaded(final int adBreakIndex, final int adIndexInAdBreak, final Ad ad) { + if (released) { + return; + } + playerHandler.post(new Runnable() { + @Override + public void run() { + if (released) { + return; + } + ImaAdsMediaSource.this.onAdLoaded(adBreakIndex, adIndexInAdBreak, ad); + } + }); + } + + @Override + public void onAdBreakPlayedToEnd(final int adBreakIndex) { + if (released) { + return; + } + playerHandler.post(new Runnable() { + @Override + public void run() { + if (released) { + return; + } + ImaAdsMediaSource.this.onAdBreakPlayedToEnd(adBreakIndex); + } + }); + } + + @Override + public void onLoadError(final IOException error) { + if (released) { + return; + } + playerHandler.post(new Runnable() { + @Override + public void run() { + if (released) { + return; + } + adLoadError = error; + } + }); + } + + } + + private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { + + private final int index; + private final Allocator allocator; + private final long positionUs; + + public MediaPeriod mediaPeriod; + private MediaPeriod.Callback callback; + + public DeferredMediaPeriod(int index, Allocator allocator, long positionUs) { + this.index = index; + this.allocator = allocator; + this.positionUs = positionUs; + } + + public void setMediaSource(MediaSource mediaSource) { + mediaPeriod = mediaSource.createPeriod(index, allocator, positionUs); + if (callback != null) { + mediaPeriod.prepare(this); + } + } + + @Override + public void prepare(Callback callback) { + this.callback = callback; + if (mediaPeriod != null) { + mediaPeriod.prepare(this); + } + } + + @Override + public void maybeThrowPrepareError() throws IOException { + if (mediaPeriod != null) { + mediaPeriod.maybeThrowPrepareError(); + } + } + + @Override + public TrackGroupArray getTrackGroups() { + return mediaPeriod.getTrackGroups(); + } + + @Override + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, + positionUs); + } + + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + + @Override + public long readDiscontinuity() { + return mediaPeriod.readDiscontinuity(); + } + + @Override + public long getBufferedPositionUs() { + return mediaPeriod.getBufferedPositionUs(); + } + + @Override + public long seekToUs(long positionUs) { + return mediaPeriod.seekToUs(positionUs); + } + + @Override + public long getNextLoadPositionUs() { + return mediaPeriod.getNextLoadPositionUs(); + } + + @Override + public boolean continueLoading(long positionUs) { + return mediaPeriod.continueLoading(positionUs); + } + + // MediaPeriod.Callback implementation. + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + Assertions.checkArgument(this.mediaPeriod == mediaPeriod); + callback.onPrepared(this); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod mediaPeriod) { + Assertions.checkArgument(this.mediaPeriod == mediaPeriod); + callback.onContinueLoadingRequested(this); + } + + } + +} diff --git a/settings.gradle b/settings.gradle index 544d2d4a21..d50cb9d3dd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,6 +23,7 @@ include ':playbacktests' include ':extension-ffmpeg' include ':extension-flac' include ':extension-gvr' +include ':extension-ima' include ':extension-okhttp' include ':extension-opus' include ':extension-vp9' @@ -38,6 +39,7 @@ project(':library-ui').projectDir = new File(settingsDir, 'library/ui') project(':extension-ffmpeg').projectDir = new File(settingsDir, 'extensions/ffmpeg') project(':extension-flac').projectDir = new File(settingsDir, 'extensions/flac') project(':extension-gvr').projectDir = new File(settingsDir, 'extensions/gvr') +project(':extension-ima').projectDir = new File(settingsDir, 'extensions/ima') project(':extension-okhttp').projectDir = new File(settingsDir, 'extensions/okhttp') project(':extension-opus').projectDir = new File(settingsDir, 'extensions/opus') project(':extension-vp9').projectDir = new File(settingsDir, 'extensions/vp9') From 16445356dce967bcb7efabb8d50ac53394eccbc5 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Wed, 17 May 2017 13:37:09 -0700 Subject: [PATCH 047/220] Fix broken link --- extensions/ima/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ima/README.md b/extensions/ima/README.md index 1dbdfd7f9b..c6f2179d0c 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -7,7 +7,7 @@ The IMA extension is a [MediaSource][] implementation wrapping the alongside content. [IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/ -[MediaSource]: https://github.com/google/ExoPlayer/blob/release-v2/library/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +[MediaSource]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java ## Using the extension ## From a6c088050a1987536b673dc1756afb5d28e48f21 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Wed, 17 May 2017 13:46:20 -0700 Subject: [PATCH 048/220] Update IMA readme --- extensions/ima/README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/extensions/ima/README.md b/extensions/ima/README.md index c6f2179d0c..19fe604786 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -22,10 +22,12 @@ pass the `ImaAdsMediaSource` to `ExoPlayer.prepare`. This is a preview version with some known issues: -* Midroll ads are not yet fully supported. In particular, seeking with midroll -ads is not yet supported. Played ad periods are not removed. Also, `playAd` and -`AD_STARTED` events are sometimes delayed, meaning that midroll ads take a long -time to start and the ad overlay does not show immediately. +* Seeking is not yet ad aware. This means that it's possible to seek back into + ads that have already been played, and also seek past midroll ads without + them being played. Seeking will be made ad aware for the first stable release. +* Midroll ads are not yet fully supported. `playAd` and `AD_STARTED` events are + sometimes delayed, meaning that midroll ads take a long time to start and the + ad overlay does not show immediately. * Tapping the 'More info' button on an ad in the demo app will pause the -activity, which destroys the ImaAdsMediaSource. Played ad breaks will be -shown to the user again if the demo app returns to the foreground. + activity, which destroys the ImaAdsMediaSource. Played ad breaks will be + shown to the user again if the demo app returns to the foreground. From 27698496fe8cb319a9c982de3307aaea348898fa Mon Sep 17 00:00:00 2001 From: ojw28 Date: Wed, 17 May 2017 13:51:36 -0700 Subject: [PATCH 049/220] Update README.md --- extensions/ima/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/extensions/ima/README.md b/extensions/ima/README.md index 19fe604786..8fb0e500c9 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -16,6 +16,11 @@ along with a `ViewGroup` that is on top of the player and the ad tag URI to show. The IMA documentation includes some [sample ad tags][] for testing. Then pass the `ImaAdsMediaSource` to `ExoPlayer.prepare`. +You can try the IMA extension in the ExoPlayer demo app. To do this you must +select and build one of the `withExtensions` build variants of the demo app in +Android Studio. In the app you can find test content in the "IMA sample ad tags" +section. + [sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags ## Known issues ## From d496621e4b60b4ff763d25174f08044b6f91a580 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Wed, 17 May 2017 13:52:19 -0700 Subject: [PATCH 050/220] Update README.md --- extensions/ima/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/ima/README.md b/extensions/ima/README.md index 8fb0e500c9..f74e927389 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -18,8 +18,8 @@ pass the `ImaAdsMediaSource` to `ExoPlayer.prepare`. You can try the IMA extension in the ExoPlayer demo app. To do this you must select and build one of the `withExtensions` build variants of the demo app in -Android Studio. In the app you can find test content in the "IMA sample ad tags" -section. +Android Studio. In the app you can find IMA test content in the "IMA sample ad +tags" section. [sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags From 6fdfdce19c568a18c7915a7b3c228e87a0383f41 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Wed, 17 May 2017 13:53:29 -0700 Subject: [PATCH 051/220] Update README.md --- extensions/ima/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/ima/README.md b/extensions/ima/README.md index f74e927389..9c0517b598 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -18,8 +18,8 @@ pass the `ImaAdsMediaSource` to `ExoPlayer.prepare`. You can try the IMA extension in the ExoPlayer demo app. To do this you must select and build one of the `withExtensions` build variants of the demo app in -Android Studio. In the app you can find IMA test content in the "IMA sample ad -tags" section. +Android Studio. You can find IMA test content in the "IMA sample ad tags" +section of the demo app. [sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags From 60ebe15c4b35d215f6af524c381e5184d60928f8 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Wed, 17 May 2017 13:55:24 -0700 Subject: [PATCH 052/220] Update README.md --- extensions/ima/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ima/README.md b/extensions/ima/README.md index 9c0517b598..aabe84136f 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -19,7 +19,7 @@ pass the `ImaAdsMediaSource` to `ExoPlayer.prepare`. You can try the IMA extension in the ExoPlayer demo app. To do this you must select and build one of the `withExtensions` build variants of the demo app in Android Studio. You can find IMA test content in the "IMA sample ad tags" -section of the demo app. +section of the app. [sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags From 1687f1653eadc4eb22177f029d27b5179c2cbf5e Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 22 May 2017 01:40:15 -0700 Subject: [PATCH 053/220] Make the error messages the first parameter in Assertions methods to match JUnit methods Reverse parameter order creates a slight confusion. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156712385 --- .../exoplayer2/demo/SampleChooserActivity.java | 16 ++++++++-------- .../exoplayer2/extractor/mp4/AtomParsers.java | 16 ++++++++-------- .../extractor/mp4/FragmentedMp4Extractor.java | 2 +- .../exoplayer2/extractor/ts/SeiReader.java | 6 +++--- .../source/ConcatenatingMediaSource.java | 4 ++-- .../exoplayer2/source/LoopingMediaSource.java | 4 ++-- .../android/exoplayer2/util/Assertions.java | 16 ++++++++-------- .../android/exoplayer2/util/LibraryLoader.java | 2 +- .../android/exoplayer2/video/DummySurface.java | 14 +++++++------- 9 files changed, 40 insertions(+), 40 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 87b8e92e83..ed9dd63a45 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -200,17 +200,17 @@ public class SampleChooserActivity extends Activity { extension = reader.nextString(); break; case "drm_scheme": - Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme"); + Assertions.checkState("Invalid attribute on nested item: drm_scheme", !insidePlaylist); drmUuid = getDrmUuid(reader.nextString()); break; case "drm_license_url": - Assertions.checkState(!insidePlaylist, - "Invalid attribute on nested item: drm_license_url"); + Assertions.checkState("Invalid attribute on nested item: drm_license_url", + !insidePlaylist); drmLicenseUrl = reader.nextString(); break; case "drm_key_request_properties": - Assertions.checkState(!insidePlaylist, - "Invalid attribute on nested item: drm_key_request_properties"); + Assertions.checkState("Invalid attribute on nested item: drm_key_request_properties", + !insidePlaylist); ArrayList drmKeyRequestPropertiesList = new ArrayList<>(); reader.beginObject(); while (reader.hasNext()) { @@ -221,12 +221,12 @@ public class SampleChooserActivity extends Activity { drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]); break; case "prefer_extension_decoders": - Assertions.checkState(!insidePlaylist, - "Invalid attribute on nested item: prefer_extension_decoders"); + Assertions.checkState("Invalid attribute on nested item: prefer_extension_decoders", + !insidePlaylist); preferExtensionDecoders = reader.nextBoolean(); break; case "playlist": - Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists"); + Assertions.checkState("Invalid nesting of playlists", !insidePlaylist); playlistSamples = new ArrayList<>(); reader.beginArray(); while (reader.hasNext()) { 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 474ba65d86..229cf8fcbd 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 @@ -593,7 +593,7 @@ import java.util.List; for (int i = 0; i < numberOfEntries; i++) { int childStartPosition = stsd.getPosition(); int childAtomSize = stsd.readInt(); - Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + Assertions.checkArgument("childAtomSize should be positive", childAtomSize > 0); int childAtomType = stsd.readInt(); if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3 || childAtomType == Atom.TYPE_encv || childAtomType == Atom.TYPE_mp4v @@ -693,7 +693,7 @@ import java.util.List; // Handle optional terminating four zero bytes in MOV files. break; } - Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + Assertions.checkArgument("childAtomSize should be positive", childAtomSize > 0); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_avcC) { Assertions.checkState(mimeType == null); @@ -877,7 +877,7 @@ import java.util.List; while (childPosition - position < size) { parent.setPosition(childPosition); int childAtomSize = parent.readInt(); - Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + Assertions.checkArgument("childAtomSize should be positive", childAtomSize > 0); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_esds || (isQuickTime && childAtomType == Atom.TYPE_wave)) { int esdsAtomPosition = childAtomType == Atom.TYPE_esds ? childPosition @@ -936,7 +936,7 @@ import java.util.List; while (childAtomPosition - position < size) { parent.setPosition(childAtomPosition); int childAtomSize = parent.readInt(); - Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + Assertions.checkArgument("childAtomSize should be positive", childAtomSize > 0); int childType = parent.readInt(); if (childType == Atom.TYPE_esds) { return childAtomPosition; @@ -1032,7 +1032,7 @@ import java.util.List; while (childPosition - position < size) { parent.setPosition(childPosition); int childAtomSize = parent.readInt(); - Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + Assertions.checkArgument("childAtomSize should be positive", childAtomSize > 0); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_sinf) { Pair result = parseSinfFromParent(parent, childPosition, @@ -1071,8 +1071,8 @@ import java.util.List; } if (isCencScheme) { - Assertions.checkArgument(dataFormat != null, "frma atom is mandatory"); - Assertions.checkArgument(trackEncryptionBox != null, "schi->tenc atom is mandatory"); + Assertions.checkArgument("frma atom is mandatory", dataFormat != null); + Assertions.checkArgument("schi->tenc atom is mandatory", trackEncryptionBox != null); return Pair.create(dataFormat, trackEncryptionBox); } else { return null; @@ -1157,7 +1157,7 @@ import java.util.List; length = chunkOffsets.readUnsignedIntToInt(); stsc.setPosition(Atom.FULL_HEADER_SIZE); remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt(); - Assertions.checkState(stsc.readInt() == 1, "first_chunk must be 1"); + Assertions.checkState("first_chunk must be 1", stsc.readInt() == 1); index = C.INDEX_UNSET; } 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 fe1d4b04af..1a40310146 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 @@ -388,7 +388,7 @@ public final class FragmentedMp4Extractor implements Extractor { } private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException { - Assertions.checkState(sideloadedTrack == null, "Unexpected moov box."); + Assertions.checkState("Unexpected moov box.", sideloadedTrack == null); DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren); 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 1e5d480ea1..5fa33b31e8 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 @@ -48,9 +48,9 @@ import java.util.List; TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); Format channelFormat = closedCaptionFormats.get(i); String channelMimeType = channelFormat.sampleMimeType; - Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType) - || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), - "Invalid closed caption mime type provided: " + channelMimeType); + Assertions.checkArgument("Invalid closed caption mime type provided: " + channelMimeType, + MimeTypes.APPLICATION_CEA608.equals(channelMimeType) + || MimeTypes.APPLICATION_CEA708.equals(channelMimeType)); output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), channelMimeType, null, Format.NO_VALUE, channelFormat.selectionFlags, channelFormat.language, channelFormat.accessibilityChannel, null)); 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 2299e757d7..c9ffa9251f 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 @@ -170,8 +170,8 @@ public final class ConcatenatingMediaSource implements MediaSource { for (int i = 0; i < timelines.length; i++) { Timeline timeline = timelines[i]; periodCount += timeline.getPeriodCount(); - Assertions.checkState(periodCount <= Integer.MAX_VALUE, - "ConcatenatingMediaSource children contain too many periods"); + Assertions.checkState("ConcatenatingMediaSource children contain too many periods", + periodCount <= Integer.MAX_VALUE); sourcePeriodOffsets[i] = (int) periodCount; windowCount += timeline.getWindowCount(); sourceWindowOffsets[i] = windowCount; 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 0e1e7d9033..fed05d1bc9 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 @@ -102,8 +102,8 @@ public final class LoopingMediaSource implements MediaSource { childPeriodCount = childTimeline.getPeriodCount(); childWindowCount = childTimeline.getWindowCount(); this.loopCount = loopCount; - Assertions.checkState(loopCount <= Integer.MAX_VALUE / childPeriodCount, - "LoopingMediaSource contains too many periods"); + Assertions.checkState("LoopingMediaSource contains too many periods", + loopCount <= Integer.MAX_VALUE / childPeriodCount); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java index aee46eea0e..afee5ee725 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java @@ -41,12 +41,12 @@ public final class Assertions { /** * Throws {@link IllegalArgumentException} if {@code expression} evaluates to false. * - * @param expression The expression to evaluate. * @param errorMessage The exception message if an exception is thrown. The message is converted * to a {@link String} using {@link String#valueOf(Object)}. + * @param expression The expression to evaluate. * @throws IllegalArgumentException If {@code expression} is false. */ - public static void checkArgument(boolean expression, Object errorMessage) { + public static void checkArgument(Object errorMessage, boolean expression) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) { throw new IllegalArgumentException(String.valueOf(errorMessage)); } @@ -83,12 +83,12 @@ public final class Assertions { /** * Throws {@link IllegalStateException} if {@code expression} evaluates to false. * - * @param expression The expression to evaluate. * @param errorMessage The exception message if an exception is thrown. The message is converted * to a {@link String} using {@link String#valueOf(Object)}. + * @param expression The expression to evaluate. * @throws IllegalStateException If {@code expression} is false. */ - public static void checkState(boolean expression, Object errorMessage) { + public static void checkState(Object errorMessage, boolean expression) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) { throw new IllegalStateException(String.valueOf(errorMessage)); } @@ -113,13 +113,13 @@ public final class Assertions { * Throws {@link NullPointerException} if {@code reference} is null. * * @param The type of the reference. - * @param reference The reference. * @param errorMessage The exception message to use if the check fails. The message is converted * to a string using {@link String#valueOf(Object)}. + * @param reference The reference. * @return The non-null reference that was validated. * @throws NullPointerException If {@code reference} is null. */ - public static T checkNotNull(T reference, Object errorMessage) { + public static T checkNotNull(Object errorMessage, T reference) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { throw new NullPointerException(String.valueOf(errorMessage)); } @@ -143,13 +143,13 @@ public final class Assertions { /** * Throws {@link IllegalArgumentException} if {@code string} is null or zero length. * - * @param string The string to check. * @param errorMessage The exception message to use if the check fails. The message is converted * to a string using {@link String#valueOf(Object)}. + * @param string The string to check. * @return The non-null, non-empty string that was validated. * @throws IllegalArgumentException If {@code string} is null or 0-length. */ - public static String checkNotEmpty(String string, Object errorMessage) { + public static String checkNotEmpty(Object errorMessage, String string) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) { throw new IllegalArgumentException(String.valueOf(errorMessage)); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java index c12bae0a07..f90817f062 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java @@ -36,7 +36,7 @@ public final class LibraryLoader { * {@link #isAvailable()}. */ public synchronized void setLibraries(String... libraries) { - Assertions.checkState(!loadAttempted, "Cannot set libraries after loading"); + Assertions.checkState("Cannot set libraries after loading", !loadAttempted); nativeLibraries = libraries; } 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 5298c82f61..2c99a6bad4 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 @@ -226,11 +226,11 @@ public final class DummySurface extends Surface { private void initInternal(boolean secure) { EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); - Assertions.checkState(display != null, "eglGetDisplay failed"); + Assertions.checkState("eglGetDisplay failed", display != null); int[] version = new int[2]; boolean eglInitialized = eglInitialize(display, version, 0, version, 1); - Assertions.checkState(eglInitialized, "eglInitialize failed"); + Assertions.checkState("eglInitialize failed", eglInitialized); int[] eglAttributes = new int[] { EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, @@ -247,8 +247,8 @@ public final class DummySurface extends Surface { int[] numConfigs = new int[1]; boolean eglChooseConfigSuccess = eglChooseConfig(display, eglAttributes, 0, configs, 0, 1, numConfigs, 0); - Assertions.checkState(eglChooseConfigSuccess && numConfigs[0] > 0 && configs[0] != null, - "eglChooseConfig failed"); + Assertions.checkState("eglChooseConfig failed", + eglChooseConfigSuccess && numConfigs[0] > 0 && configs[0] != null); EGLConfig config = configs[0]; int[] glAttributes; @@ -264,7 +264,7 @@ public final class DummySurface extends Surface { } EGLContext context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0); - Assertions.checkState(context != null, "eglCreateContext failed"); + Assertions.checkState("eglCreateContext failed", context != null); int[] pbufferAttributes; if (secure) { @@ -280,10 +280,10 @@ public final class DummySurface extends Surface { EGL_NONE}; } EGLSurface pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); - Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); + Assertions.checkState("eglCreatePbufferSurface failed", pbuffer != null); boolean eglMadeCurrent = eglMakeCurrent(display, pbuffer, pbuffer, context); - Assertions.checkState(eglMadeCurrent, "eglMakeCurrent failed"); + Assertions.checkState("eglMakeCurrent failed", eglMadeCurrent); glGenTextures(1, textureIdHolder, 0); surfaceTexture = new SurfaceTexture(textureIdHolder[0]); From 78c4cb74ea3ec0076bb17307b4850dce3b733b56 Mon Sep 17 00:00:00 2001 From: andrewrice Date: Mon, 22 May 2017 08:22:10 -0700 Subject: [PATCH 054/220] Fixes incorrectly-ordered arguments to calls to assertEquals ([] This change has been automatically generated by an Error Prone check that detects incorrect argument ordering on calls to assertEquals-style methods. See [] Cleanup change automatically generated by javacflume/refactory Refactoring: third_party/java_src/error_prone/project/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects:AssertEqualsArgumentOrderChecker_refactoring Tested: TAP --sample for global presubmit queue [] ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156739144 --- .../exoplayer2/text/webvtt/CssParserTest.java | 38 +++++++++---------- .../upstream/cache/CacheDataSourceTest.java | 18 ++++----- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java index f471370e4c..d6be100877 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java @@ -125,25 +125,25 @@ public final class CssParserTest extends InstrumentationTestCase { String stringInput = " lorem:ipsum\n{dolor}#sit,amet;lorem:ipsum\r\t\f\ndolor(())\n"; ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(stringInput)); StringBuilder builder = new StringBuilder(); - assertEquals(CssParser.parseNextToken(input, builder), "lorem"); - assertEquals(CssParser.parseNextToken(input, builder), ":"); - assertEquals(CssParser.parseNextToken(input, builder), "ipsum"); - assertEquals(CssParser.parseNextToken(input, builder), "{"); - assertEquals(CssParser.parseNextToken(input, builder), "dolor"); - assertEquals(CssParser.parseNextToken(input, builder), "}"); - assertEquals(CssParser.parseNextToken(input, builder), "#sit"); - assertEquals(CssParser.parseNextToken(input, builder), ","); - assertEquals(CssParser.parseNextToken(input, builder), "amet"); - assertEquals(CssParser.parseNextToken(input, builder), ";"); - assertEquals(CssParser.parseNextToken(input, builder), "lorem"); - assertEquals(CssParser.parseNextToken(input, builder), ":"); - assertEquals(CssParser.parseNextToken(input, builder), "ipsum"); - assertEquals(CssParser.parseNextToken(input, builder), "dolor"); - assertEquals(CssParser.parseNextToken(input, builder), "("); - assertEquals(CssParser.parseNextToken(input, builder), "("); - assertEquals(CssParser.parseNextToken(input, builder), ")"); - assertEquals(CssParser.parseNextToken(input, builder), ")"); - assertEquals(CssParser.parseNextToken(input, builder), null); + assertEquals("lorem", CssParser.parseNextToken(input, builder)); + assertEquals(":", CssParser.parseNextToken(input, builder)); + assertEquals("ipsum", CssParser.parseNextToken(input, builder)); + assertEquals("{", CssParser.parseNextToken(input, builder)); + assertEquals("dolor", CssParser.parseNextToken(input, builder)); + assertEquals("}", CssParser.parseNextToken(input, builder)); + assertEquals("#sit", CssParser.parseNextToken(input, builder)); + assertEquals(",", CssParser.parseNextToken(input, builder)); + assertEquals("amet", CssParser.parseNextToken(input, builder)); + assertEquals(";", CssParser.parseNextToken(input, builder)); + assertEquals("lorem", CssParser.parseNextToken(input, builder)); + assertEquals(":", CssParser.parseNextToken(input, builder)); + assertEquals("ipsum", CssParser.parseNextToken(input, builder)); + assertEquals("dolor", CssParser.parseNextToken(input, builder)); + assertEquals("(", CssParser.parseNextToken(input, builder)); + assertEquals("(", CssParser.parseNextToken(input, builder)); + assertEquals(")", CssParser.parseNextToken(input, builder)); + assertEquals(")", CssParser.parseNextToken(input, builder)); + assertEquals(null, CssParser.parseNextToken(input, builder)); } public void testStyleScoreSystem() { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index c76e4989d8..5c342ae3d3 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -160,25 +160,25 @@ public class CacheDataSourceTest extends InstrumentationTestCase { private void assertReadData(CacheDataSource cacheDataSource, boolean unknownLength, int position, int length) throws IOException { - int actualLength = TEST_DATA.length - position; + int testDataLength = TEST_DATA.length - position; if (length != C.LENGTH_UNSET) { - actualLength = Math.min(actualLength, length); + testDataLength = Math.min(testDataLength, length); } - assertEquals(unknownLength ? length : actualLength, + assertEquals(unknownLength ? length : testDataLength, cacheDataSource.open(new DataSpec(Uri.EMPTY, position, length, KEY_1))); byte[] buffer = new byte[100]; - int index = 0; + int totalBytesRead = 0; while (true) { - int read = cacheDataSource.read(buffer, index, buffer.length - index); + int read = cacheDataSource.read(buffer, totalBytesRead, buffer.length - totalBytesRead); if (read == C.RESULT_END_OF_INPUT) { break; } - index += read; + totalBytesRead += read; } - assertEquals(actualLength, index); - MoreAsserts.assertEquals(Arrays.copyOfRange(TEST_DATA, position, position + actualLength), - Arrays.copyOf(buffer, index)); + assertEquals(testDataLength, totalBytesRead); + MoreAsserts.assertEquals(Arrays.copyOfRange(TEST_DATA, position, position + testDataLength), + Arrays.copyOf(buffer, totalBytesRead)); cacheDataSource.close(); } From e892e3a5c71ac4543f49d6e8b35359ee62a0f523 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 22 May 2017 13:52:51 -0700 Subject: [PATCH 055/220] Fix TTML positioning Issue: #2824 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156781252 --- .../exoplayer2/text/ttml/TtmlDecoderTest.java | 34 +++--- .../exoplayer2/text/ttml/TtmlDecoder.java | 100 +++++++++++++----- .../exoplayer2/text/ttml/TtmlNode.java | 7 +- .../exoplayer2/text/ttml/TtmlRegion.java | 14 ++- 4 files changed, 101 insertions(+), 54 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java index 381aaa34ae..496e3f87de 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java @@ -157,39 +157,39 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { assertEquals(2, output.size()); Cue ttmlCue = output.get(0); assertEquals("lorem", ttmlCue.text.toString()); - assertEquals(10.f / 100.f, ttmlCue.position); - assertEquals(10.f / 100.f, ttmlCue.line); - assertEquals(20.f / 100.f, ttmlCue.size); + assertEquals(10f / 100f, ttmlCue.position); + assertEquals(10f / 100f, ttmlCue.line); + assertEquals(20f / 100f, ttmlCue.size); ttmlCue = output.get(1); assertEquals("amet", ttmlCue.text.toString()); - assertEquals(60.f / 100.f, ttmlCue.position); - assertEquals(10.f / 100.f, ttmlCue.line); - assertEquals(20.f / 100.f, ttmlCue.size); + assertEquals(60f / 100f, ttmlCue.position); + assertEquals(10f / 100f, ttmlCue.line); + assertEquals(20f / 100f, ttmlCue.size); output = subtitle.getCues(5000000); assertEquals(1, output.size()); ttmlCue = output.get(0); assertEquals("ipsum", ttmlCue.text.toString()); - assertEquals(40.f / 100.f, ttmlCue.position); - assertEquals(40.f / 100.f, ttmlCue.line); - assertEquals(20.f / 100.f, ttmlCue.size); + assertEquals(40f / 100f, ttmlCue.position); + assertEquals(40f / 100f, ttmlCue.line); + assertEquals(20f / 100f, ttmlCue.size); output = subtitle.getCues(9000000); assertEquals(1, output.size()); ttmlCue = output.get(0); assertEquals("dolor", ttmlCue.text.toString()); - assertEquals(10.f / 100.f, ttmlCue.position); - assertEquals(80.f / 100.f, ttmlCue.line); - assertEquals(Cue.DIMEN_UNSET, ttmlCue.size); + assertEquals(10f / 100f, ttmlCue.position); + assertEquals(80f / 100f, ttmlCue.line); + assertEquals(1f, ttmlCue.size); output = subtitle.getCues(21000000); assertEquals(1, output.size()); ttmlCue = output.get(0); assertEquals("She first said this", ttmlCue.text.toString()); - assertEquals(45.f / 100.f, ttmlCue.position); - assertEquals(45.f / 100.f, ttmlCue.line); - assertEquals(35.f / 100.f, ttmlCue.size); + assertEquals(45f / 100f, ttmlCue.position); + assertEquals(45f / 100f, ttmlCue.line); + assertEquals(35f / 100f, ttmlCue.size); output = subtitle.getCues(25000000); ttmlCue = output.get(0); assertEquals("She first said this\nThen this", ttmlCue.text.toString()); @@ -197,8 +197,8 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { assertEquals(1, output.size()); ttmlCue = output.get(0); assertEquals("She first said this\nThen this\nFinally this", ttmlCue.text.toString()); - assertEquals(45.f / 100.f, ttmlCue.position); - assertEquals(45.f / 100.f, ttmlCue.line); + assertEquals(45f / 100f, ttmlCue.position); + assertEquals(45f / 100f, ttmlCue.line); } public void testEmptyStyleAttribute() throws IOException, SubtitleDecoderException { 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 71ce17eeed..0012ce2c22 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 @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.text.ttml; import android.text.Layout; import android.util.Log; -import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; @@ -100,7 +99,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { XmlPullParser xmlParser = xmlParserFactory.newPullParser(); Map globalStyles = new HashMap<>(); Map regionMap = new HashMap<>(); - regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion()); + regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion(null)); ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length); xmlParser.setInput(inputStream, null); TtmlSubtitle ttmlSubtitle = null; @@ -211,9 +210,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { globalStyles.put(style.getId(), style); } } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) { - Pair ttmlRegionInfo = parseRegionAttributes(xmlParser); - if (ttmlRegionInfo != null) { - globalRegions.put(ttmlRegionInfo.first, ttmlRegionInfo.second); + TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser); + if (ttmlRegion != null) { + globalRegions.put(ttmlRegion.id, ttmlRegion); } } } while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD)); @@ -221,41 +220,84 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } /** - * Parses a region declaration. Supports origin and extent definition but only when defined in - * terms of percentage of the viewport. Regions that do not correctly declare origin are ignored. + * Parses a region declaration. + *

      + * If the region defines an origin and/or extent, it is required that they're defined as + * percentages of the viewport. Region declarations that define origin and/or extent in other + * formats are unsupported, and null is returned. */ - private Pair parseRegionAttributes(XmlPullParser xmlParser) { + private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser) { String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID); - String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN); - String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT); - if (regionOrigin == null || regionId == null) { + if (regionId == null) { return null; } - float position = Cue.DIMEN_UNSET; - float line = Cue.DIMEN_UNSET; - Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin); - if (originMatcher.matches()) { - try { - position = Float.parseFloat(originMatcher.group(1)) / 100.f; - line = Float.parseFloat(originMatcher.group(2)) / 100.f; - } catch (NumberFormatException e) { - Log.w(TAG, "Ignoring region with malformed origin: '" + regionOrigin + "'", e); - position = Cue.DIMEN_UNSET; + + float position; + float line; + String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN); + if (regionOrigin != null) { + Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin); + if (originMatcher.matches()) { + try { + position = Float.parseFloat(originMatcher.group(1)) / 100f; + line = Float.parseFloat(originMatcher.group(2)) / 100f; + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin); + return null; + } + } else { + Log.w(TAG, "Ignoring region with unsupported origin: " + regionOrigin); + return null; } + } else { + // Origin is omitted. Default to top left. + position = 0; + line = 0; } - float width = Cue.DIMEN_UNSET; + + float width; + float height; + String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT); if (regionExtent != null) { Matcher extentMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent); if (extentMatcher.matches()) { try { - width = Float.parseFloat(extentMatcher.group(1)) / 100.f; + width = Float.parseFloat(extentMatcher.group(1)) / 100f; + height = Float.parseFloat(extentMatcher.group(2)) / 100f; } catch (NumberFormatException e) { - Log.w(TAG, "Ignoring malformed region extent: '" + regionExtent + "'", e); + Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin); + return null; } + } else { + Log.w(TAG, "Ignoring region with unsupported extent: " + regionOrigin); + return null; + } + } else { + // Extent is omitted. Default to extent of parent. + width = 1; + height = 1; + } + + @Cue.AnchorType int lineAnchor = Cue.ANCHOR_TYPE_START; + String displayAlign = XmlPullParserUtil.getAttributeValue(xmlParser, + TtmlNode.ATTR_TTS_DISPLAY_ALIGN); + if (displayAlign != null) { + switch (displayAlign.toLowerCase()) { + case "center": + lineAnchor = Cue.ANCHOR_TYPE_MIDDLE; + line += height / 2; + break; + case "after": + lineAnchor = Cue.ANCHOR_TYPE_END; + line += height; + break; + default: + // Default "before" case. Do nothing. + break; } } - return position != Cue.DIMEN_UNSET ? new Pair<>(regionId, new TtmlRegion(position, line, - Cue.LINE_TYPE_FRACTION, width)) : null; + + return new TtmlRegion(regionId, position, line, Cue.LINE_TYPE_FRACTION, lineAnchor, width); } private String[] parseStyleIds(String parentStyleIds) { @@ -277,7 +319,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { try { style.setBackgroundColor(ColorParser.parseTtmlColor(attributeValue)); } catch (IllegalArgumentException e) { - Log.w(TAG, "failed parsing background value: '" + attributeValue + "'"); + Log.w(TAG, "Failed parsing background value: " + attributeValue); } break; case TtmlNode.ATTR_TTS_COLOR: @@ -285,7 +327,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { try { style.setFontColor(ColorParser.parseTtmlColor(attributeValue)); } catch (IllegalArgumentException e) { - Log.w(TAG, "failed parsing color value: '" + attributeValue + "'"); + Log.w(TAG, "Failed parsing color value: " + attributeValue); } break; case TtmlNode.ATTR_TTS_FONT_FAMILY: @@ -296,7 +338,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { style = createIfNull(style); parseFontSize(attributeValue, style); } catch (SubtitleDecoderException e) { - Log.w(TAG, "failed parsing fontSize value: '" + attributeValue + "'"); + Log.w(TAG, "Failed parsing fontSize value: " + attributeValue); } break; case TtmlNode.ATTR_TTS_FONT_WEIGHT: 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 18378df445..43fa7a1bd9 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 @@ -50,14 +50,15 @@ import java.util.TreeSet; public static final String ANONYMOUS_REGION_ID = ""; public static final String ATTR_ID = "id"; - public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor"; + public static final String ATTR_TTS_ORIGIN = "origin"; public static final String ATTR_TTS_EXTENT = "extent"; + public static final String ATTR_TTS_DISPLAY_ALIGN = "displayAlign"; + public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor"; public static final String ATTR_TTS_FONT_STYLE = "fontStyle"; public static final String ATTR_TTS_FONT_SIZE = "fontSize"; public static final String ATTR_TTS_FONT_FAMILY = "fontFamily"; public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight"; public static final String ATTR_TTS_COLOR = "color"; - public static final String ATTR_TTS_ORIGIN = "origin"; public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration"; public static final String ATTR_TTS_TEXT_ALIGN = "textAlign"; @@ -179,7 +180,7 @@ import java.util.TreeSet; for (Entry entry : regionOutputs.entrySet()) { TtmlRegion region = regionMap.get(entry.getKey()); cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, region.lineType, - Cue.TYPE_UNSET, region.position, Cue.TYPE_UNSET, region.width)); + region.lineAnchor, region.position, Cue.TYPE_UNSET, region.width)); } return 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 5f30834b4d..98823d7a84 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 @@ -22,20 +22,24 @@ import com.google.android.exoplayer2.text.Cue; */ /* package */ final class TtmlRegion { + public final String id; public final float position; public final float line; - @Cue.LineType - public final int lineType; + @Cue.LineType public final int lineType; + @Cue.AnchorType public final int lineAnchor; public final float width; - public TtmlRegion() { - this(Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); + public TtmlRegion(String id) { + this(id, Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); } - public TtmlRegion(float position, float line, @Cue.LineType int lineType, float width) { + public TtmlRegion(String id, float position, float line, @Cue.LineType int lineType, + @Cue.AnchorType int lineAnchor, float width) { + this.id = id; this.position = position; this.line = line; this.lineType = lineType; + this.lineAnchor = lineAnchor; this.width = width; } From 88fddb42eca2da65297cfacd1a068aadc47b2614 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 23 May 2017 01:43:24 -0700 Subject: [PATCH 056/220] Provide video frame timestamps to subclasses Expose the stream offset to BaseRenderer subclasses. Issue: #2267 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156837514 --- .../android/exoplayer2/BaseRenderer.java | 11 ++- .../exoplayer2/metadata/MetadataRenderer.java | 2 +- .../android/exoplayer2/text/TextRenderer.java | 2 +- .../video/MediaCodecVideoRenderer.java | 99 ++++++++++++++++--- 4 files changed, 92 insertions(+), 22 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 396584a39e..a88a1dd615 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -96,7 +96,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { this.stream = stream; readEndOfStream = false; streamOffsetUs = offsetUs; - onStreamChanged(formats); + onStreamChanged(formats, offsetUs); } @Override @@ -183,16 +183,19 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { * The default implementation is a no-op. * * @param formats The enabled formats. + * @param offsetUs The offset that will be added to the timestamps of buffers read via + * {@link #readSource(FormatHolder, DecoderInputBuffer, boolean)} so that decoder input + * buffers have monotonically increasing timestamps. * @throws ExoPlaybackException If an error occurs. */ - protected void onStreamChanged(Format[] formats) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { // Do nothing. } /** * Called when the position is reset. This occurs when the renderer is enabled after - * {@link #onStreamChanged(Format[])} has been called, and also when a position discontinuity - * is encountered. + * {@link #onStreamChanged(Format[], long)} has been called, and also when a position + * discontinuity is encountered. *

      * After a position reset, the renderer's {@link SampleStream} is guaranteed to provide samples * starting from a key frame. 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 70b2d8aab9..7ff426e2df 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 @@ -104,7 +104,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } @Override - protected void onStreamChanged(Format[] formats) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { decoder = decoderFactory.createDecoder(formats[0]); } 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 4950549b19..1820d43e75 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 @@ -130,7 +130,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { } @Override - protected void onStreamChanged(Format[] formats) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { streamFormat = formats[0]; if (decoder != null) { decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM; 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 dd0c5356ea..aabec0eaa7 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 @@ -62,11 +62,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private static final int[] STANDARD_LONG_EDGE_VIDEO_PX = new int[] { 1920, 1600, 1440, 1280, 960, 854, 640, 540, 480}; + // Generally there is zero or one pending output stream offset. We track more offsets to allow for + // pending output streams that have fewer frames than the codec latency. + private static final int MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT = 10; + private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper; private final EventDispatcher eventDispatcher; private final long allowedJoiningTimeMs; private final int maxDroppedFramesToNotify; private final boolean deviceNeedsAutoFrcWorkaround; + private final long[] pendingOutputStreamOffsetsUs; private Format[] streamFormats; private CodecMaxValues codecMaxValues; @@ -95,6 +100,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private int tunnelingAudioSessionId; /* package */ OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener; + private long outputStreamOffsetUs; + private int pendingOutputStreamOffsetCount; + /** * @param context A context. * @param mediaCodecSelector A decoder selector. @@ -160,6 +168,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context); eventDispatcher = new EventDispatcher(eventHandler, eventListener); deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround(); + pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; + outputStreamOffsetUs = C.TIME_UNSET; joiningDeadlineMs = C.TIME_UNSET; currentWidth = Format.NO_VALUE; currentHeight = Format.NO_VALUE; @@ -219,9 +229,20 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected void onStreamChanged(Format[] formats) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { streamFormats = formats; - super.onStreamChanged(formats); + if (outputStreamOffsetUs == C.TIME_UNSET) { + outputStreamOffsetUs = offsetUs; + } else { + if (pendingOutputStreamOffsetCount == pendingOutputStreamOffsetsUs.length) { + Log.w(TAG, "Too many stream changes, so dropping offset: " + + pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1]); + } else { + pendingOutputStreamOffsetCount++; + } + pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1] = offsetUs; + } + super.onStreamChanged(formats, offsetUs); } @Override @@ -229,6 +250,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { super.onPositionReset(positionUs, joining); clearRenderedFirstFrame(); consecutiveDroppedFrameCount = 0; + if (pendingOutputStreamOffsetCount != 0) { + outputStreamOffsetUs = pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1]; + pendingOutputStreamOffsetCount = 0; + } if (joining) { setJoiningDeadlineMs(); } else { @@ -275,6 +300,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { currentHeight = Format.NO_VALUE; currentPixelWidthHeightRatio = Format.NO_VALUE; pendingPixelWidthHeightRatio = Format.NO_VALUE; + outputStreamOffsetUs = C.TIME_UNSET; + pendingOutputStreamOffsetCount = 0; clearReportedVideoSize(); clearRenderedFirstFrame(); frameReleaseTimeHelper.disable(); @@ -417,16 +444,24 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, boolean shouldSkip) { + while (pendingOutputStreamOffsetCount != 0 + && bufferPresentationTimeUs >= pendingOutputStreamOffsetsUs[0]) { + outputStreamOffsetUs = pendingOutputStreamOffsetsUs[0]; + pendingOutputStreamOffsetCount--; + System.arraycopy(pendingOutputStreamOffsetsUs, 1, pendingOutputStreamOffsetsUs, 0, + pendingOutputStreamOffsetCount); + } + long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs; if (shouldSkip) { - skipOutputBuffer(codec, bufferIndex); + skipOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } if (!renderedFirstFrame) { if (Util.SDK_INT >= 21) { - renderOutputBufferV21(codec, bufferIndex, System.nanoTime()); + renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, System.nanoTime()); } else { - renderOutputBuffer(codec, bufferIndex); + renderOutputBuffer(codec, bufferIndex, presentationTimeUs); } return true; } @@ -450,14 +485,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { // We're more than 30ms late rendering the frame. - dropOutputBuffer(codec, bufferIndex); + dropOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } if (Util.SDK_INT >= 21) { // Let the underlying framework time the release. if (earlyUs < 50000) { - renderOutputBufferV21(codec, bufferIndex, adjustedReleaseTimeNs); + renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, adjustedReleaseTimeNs); return true; } } else { @@ -473,7 +508,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { Thread.currentThread().interrupt(); } } - renderOutputBuffer(codec, bufferIndex); + renderOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } } @@ -495,16 +530,30 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return earlyUs < -30000; } - private void skipOutputBuffer(MediaCodec codec, int bufferIndex) { + /** + * Skips the output buffer with the specified index. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to skip. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + */ + protected void skipOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) { TraceUtil.beginSection("skipVideoBuffer"); - codec.releaseOutputBuffer(bufferIndex, false); + codec.releaseOutputBuffer(index, false); TraceUtil.endSection(); decoderCounters.skippedOutputBufferCount++; } - private void dropOutputBuffer(MediaCodec codec, int bufferIndex) { + /** + * Drops the output buffer with the specified index. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to drop. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + */ + protected void dropOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) { TraceUtil.beginSection("dropVideoBuffer"); - codec.releaseOutputBuffer(bufferIndex, false); + codec.releaseOutputBuffer(index, false); TraceUtil.endSection(); decoderCounters.droppedOutputBufferCount++; droppedFrames++; @@ -516,21 +565,39 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } - private void renderOutputBuffer(MediaCodec codec, int bufferIndex) { + /** + * Renders the output buffer with the specified index. This method is only called if the platform + * API version of the device is less than 21. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to drop. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + */ + protected void renderOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) { maybeNotifyVideoSizeChanged(); TraceUtil.beginSection("releaseOutputBuffer"); - codec.releaseOutputBuffer(bufferIndex, true); + codec.releaseOutputBuffer(index, true); TraceUtil.endSection(); decoderCounters.renderedOutputBufferCount++; consecutiveDroppedFrameCount = 0; maybeNotifyRenderedFirstFrame(); } + /** + * Renders the output buffer with the specified index. This method is only called if the platform + * API version of the device is 21 or later. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to drop. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + * @param releaseTimeNs The wallclock time at which the frame should be displayed, in nanoseconds. + */ @TargetApi(21) - private void renderOutputBufferV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) { + protected void renderOutputBufferV21(MediaCodec codec, int index, long presentationTimeUs, + long releaseTimeNs) { maybeNotifyVideoSizeChanged(); TraceUtil.beginSection("releaseOutputBuffer"); - codec.releaseOutputBuffer(bufferIndex, releaseTimeNs); + codec.releaseOutputBuffer(index, releaseTimeNs); TraceUtil.endSection(); decoderCounters.renderedOutputBufferCount++; consecutiveDroppedFrameCount = 0; From 72dcfd19c8c0086874898f00ed7527acc16ab999 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 23 May 2017 03:32:42 -0700 Subject: [PATCH 057/220] Automated g4 rollback of changelist 156712385. *** Reason for rollback *** This change may silently introduce bugs where both parameters are acceptable in both places. It's decided that the gain isn't worth the risk. *** Original change description *** Make the error messages the first parameter in Assertions methods to match JUnit methods Reverse parameter order creates a slight confusion. *** ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156844728 --- .../exoplayer2/demo/SampleChooserActivity.java | 16 ++++++++-------- .../exoplayer2/extractor/mp4/AtomParsers.java | 16 ++++++++-------- .../extractor/mp4/FragmentedMp4Extractor.java | 2 +- .../exoplayer2/extractor/ts/SeiReader.java | 6 +++--- .../source/ConcatenatingMediaSource.java | 4 ++-- .../exoplayer2/source/LoopingMediaSource.java | 4 ++-- .../android/exoplayer2/util/Assertions.java | 16 ++++++++-------- .../android/exoplayer2/util/LibraryLoader.java | 2 +- .../android/exoplayer2/video/DummySurface.java | 14 +++++++------- 9 files changed, 40 insertions(+), 40 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index ed9dd63a45..87b8e92e83 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -200,17 +200,17 @@ public class SampleChooserActivity extends Activity { extension = reader.nextString(); break; case "drm_scheme": - Assertions.checkState("Invalid attribute on nested item: drm_scheme", !insidePlaylist); + Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme"); drmUuid = getDrmUuid(reader.nextString()); break; case "drm_license_url": - Assertions.checkState("Invalid attribute on nested item: drm_license_url", - !insidePlaylist); + Assertions.checkState(!insidePlaylist, + "Invalid attribute on nested item: drm_license_url"); drmLicenseUrl = reader.nextString(); break; case "drm_key_request_properties": - Assertions.checkState("Invalid attribute on nested item: drm_key_request_properties", - !insidePlaylist); + Assertions.checkState(!insidePlaylist, + "Invalid attribute on nested item: drm_key_request_properties"); ArrayList drmKeyRequestPropertiesList = new ArrayList<>(); reader.beginObject(); while (reader.hasNext()) { @@ -221,12 +221,12 @@ public class SampleChooserActivity extends Activity { drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]); break; case "prefer_extension_decoders": - Assertions.checkState("Invalid attribute on nested item: prefer_extension_decoders", - !insidePlaylist); + Assertions.checkState(!insidePlaylist, + "Invalid attribute on nested item: prefer_extension_decoders"); preferExtensionDecoders = reader.nextBoolean(); break; case "playlist": - Assertions.checkState("Invalid nesting of playlists", !insidePlaylist); + Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists"); playlistSamples = new ArrayList<>(); reader.beginArray(); while (reader.hasNext()) { 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 229cf8fcbd..474ba65d86 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 @@ -593,7 +593,7 @@ import java.util.List; for (int i = 0; i < numberOfEntries; i++) { int childStartPosition = stsd.getPosition(); int childAtomSize = stsd.readInt(); - Assertions.checkArgument("childAtomSize should be positive", childAtomSize > 0); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = stsd.readInt(); if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3 || childAtomType == Atom.TYPE_encv || childAtomType == Atom.TYPE_mp4v @@ -693,7 +693,7 @@ import java.util.List; // Handle optional terminating four zero bytes in MOV files. break; } - Assertions.checkArgument("childAtomSize should be positive", childAtomSize > 0); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_avcC) { Assertions.checkState(mimeType == null); @@ -877,7 +877,7 @@ import java.util.List; while (childPosition - position < size) { parent.setPosition(childPosition); int childAtomSize = parent.readInt(); - Assertions.checkArgument("childAtomSize should be positive", childAtomSize > 0); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_esds || (isQuickTime && childAtomType == Atom.TYPE_wave)) { int esdsAtomPosition = childAtomType == Atom.TYPE_esds ? childPosition @@ -936,7 +936,7 @@ import java.util.List; while (childAtomPosition - position < size) { parent.setPosition(childAtomPosition); int childAtomSize = parent.readInt(); - Assertions.checkArgument("childAtomSize should be positive", childAtomSize > 0); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); int childType = parent.readInt(); if (childType == Atom.TYPE_esds) { return childAtomPosition; @@ -1032,7 +1032,7 @@ import java.util.List; while (childPosition - position < size) { parent.setPosition(childPosition); int childAtomSize = parent.readInt(); - Assertions.checkArgument("childAtomSize should be positive", childAtomSize > 0); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_sinf) { Pair result = parseSinfFromParent(parent, childPosition, @@ -1071,8 +1071,8 @@ import java.util.List; } if (isCencScheme) { - Assertions.checkArgument("frma atom is mandatory", dataFormat != null); - Assertions.checkArgument("schi->tenc atom is mandatory", trackEncryptionBox != null); + Assertions.checkArgument(dataFormat != null, "frma atom is mandatory"); + Assertions.checkArgument(trackEncryptionBox != null, "schi->tenc atom is mandatory"); return Pair.create(dataFormat, trackEncryptionBox); } else { return null; @@ -1157,7 +1157,7 @@ import java.util.List; length = chunkOffsets.readUnsignedIntToInt(); stsc.setPosition(Atom.FULL_HEADER_SIZE); remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt(); - Assertions.checkState("first_chunk must be 1", stsc.readInt() == 1); + Assertions.checkState(stsc.readInt() == 1, "first_chunk must be 1"); index = C.INDEX_UNSET; } 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 1a40310146..fe1d4b04af 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 @@ -388,7 +388,7 @@ public final class FragmentedMp4Extractor implements Extractor { } private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException { - Assertions.checkState("Unexpected moov box.", sideloadedTrack == null); + Assertions.checkState(sideloadedTrack == null, "Unexpected moov box."); DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren); 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 5fa33b31e8..1e5d480ea1 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 @@ -48,9 +48,9 @@ import java.util.List; TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); Format channelFormat = closedCaptionFormats.get(i); String channelMimeType = channelFormat.sampleMimeType; - Assertions.checkArgument("Invalid closed caption mime type provided: " + channelMimeType, - MimeTypes.APPLICATION_CEA608.equals(channelMimeType) - || MimeTypes.APPLICATION_CEA708.equals(channelMimeType)); + Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType) + || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), + "Invalid closed caption mime type provided: " + channelMimeType); output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), channelMimeType, null, Format.NO_VALUE, channelFormat.selectionFlags, channelFormat.language, channelFormat.accessibilityChannel, null)); 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 c9ffa9251f..2299e757d7 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 @@ -170,8 +170,8 @@ public final class ConcatenatingMediaSource implements MediaSource { for (int i = 0; i < timelines.length; i++) { Timeline timeline = timelines[i]; periodCount += timeline.getPeriodCount(); - Assertions.checkState("ConcatenatingMediaSource children contain too many periods", - periodCount <= Integer.MAX_VALUE); + Assertions.checkState(periodCount <= Integer.MAX_VALUE, + "ConcatenatingMediaSource children contain too many periods"); sourcePeriodOffsets[i] = (int) periodCount; windowCount += timeline.getWindowCount(); sourceWindowOffsets[i] = windowCount; 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 fed05d1bc9..0e1e7d9033 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 @@ -102,8 +102,8 @@ public final class LoopingMediaSource implements MediaSource { childPeriodCount = childTimeline.getPeriodCount(); childWindowCount = childTimeline.getWindowCount(); this.loopCount = loopCount; - Assertions.checkState("LoopingMediaSource contains too many periods", - loopCount <= Integer.MAX_VALUE / childPeriodCount); + Assertions.checkState(loopCount <= Integer.MAX_VALUE / childPeriodCount, + "LoopingMediaSource contains too many periods"); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java index afee5ee725..aee46eea0e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java @@ -41,12 +41,12 @@ public final class Assertions { /** * Throws {@link IllegalArgumentException} if {@code expression} evaluates to false. * + * @param expression The expression to evaluate. * @param errorMessage The exception message if an exception is thrown. The message is converted * to a {@link String} using {@link String#valueOf(Object)}. - * @param expression The expression to evaluate. * @throws IllegalArgumentException If {@code expression} is false. */ - public static void checkArgument(Object errorMessage, boolean expression) { + public static void checkArgument(boolean expression, Object errorMessage) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) { throw new IllegalArgumentException(String.valueOf(errorMessage)); } @@ -83,12 +83,12 @@ public final class Assertions { /** * Throws {@link IllegalStateException} if {@code expression} evaluates to false. * + * @param expression The expression to evaluate. * @param errorMessage The exception message if an exception is thrown. The message is converted * to a {@link String} using {@link String#valueOf(Object)}. - * @param expression The expression to evaluate. * @throws IllegalStateException If {@code expression} is false. */ - public static void checkState(Object errorMessage, boolean expression) { + public static void checkState(boolean expression, Object errorMessage) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) { throw new IllegalStateException(String.valueOf(errorMessage)); } @@ -113,13 +113,13 @@ public final class Assertions { * Throws {@link NullPointerException} if {@code reference} is null. * * @param The type of the reference. + * @param reference The reference. * @param errorMessage The exception message to use if the check fails. The message is converted * to a string using {@link String#valueOf(Object)}. - * @param reference The reference. * @return The non-null reference that was validated. * @throws NullPointerException If {@code reference} is null. */ - public static T checkNotNull(Object errorMessage, T reference) { + public static T checkNotNull(T reference, Object errorMessage) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { throw new NullPointerException(String.valueOf(errorMessage)); } @@ -143,13 +143,13 @@ public final class Assertions { /** * Throws {@link IllegalArgumentException} if {@code string} is null or zero length. * + * @param string The string to check. * @param errorMessage The exception message to use if the check fails. The message is converted * to a string using {@link String#valueOf(Object)}. - * @param string The string to check. * @return The non-null, non-empty string that was validated. * @throws IllegalArgumentException If {@code string} is null or 0-length. */ - public static String checkNotEmpty(Object errorMessage, String string) { + public static String checkNotEmpty(String string, Object errorMessage) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) { throw new IllegalArgumentException(String.valueOf(errorMessage)); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java index f90817f062..c12bae0a07 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java @@ -36,7 +36,7 @@ public final class LibraryLoader { * {@link #isAvailable()}. */ public synchronized void setLibraries(String... libraries) { - Assertions.checkState("Cannot set libraries after loading", !loadAttempted); + Assertions.checkState(!loadAttempted, "Cannot set libraries after loading"); nativeLibraries = libraries; } 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 2c99a6bad4..5298c82f61 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 @@ -226,11 +226,11 @@ public final class DummySurface extends Surface { private void initInternal(boolean secure) { EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); - Assertions.checkState("eglGetDisplay failed", display != null); + Assertions.checkState(display != null, "eglGetDisplay failed"); int[] version = new int[2]; boolean eglInitialized = eglInitialize(display, version, 0, version, 1); - Assertions.checkState("eglInitialize failed", eglInitialized); + Assertions.checkState(eglInitialized, "eglInitialize failed"); int[] eglAttributes = new int[] { EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, @@ -247,8 +247,8 @@ public final class DummySurface extends Surface { int[] numConfigs = new int[1]; boolean eglChooseConfigSuccess = eglChooseConfig(display, eglAttributes, 0, configs, 0, 1, numConfigs, 0); - Assertions.checkState("eglChooseConfig failed", - eglChooseConfigSuccess && numConfigs[0] > 0 && configs[0] != null); + Assertions.checkState(eglChooseConfigSuccess && numConfigs[0] > 0 && configs[0] != null, + "eglChooseConfig failed"); EGLConfig config = configs[0]; int[] glAttributes; @@ -264,7 +264,7 @@ public final class DummySurface extends Surface { } EGLContext context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0); - Assertions.checkState("eglCreateContext failed", context != null); + Assertions.checkState(context != null, "eglCreateContext failed"); int[] pbufferAttributes; if (secure) { @@ -280,10 +280,10 @@ public final class DummySurface extends Surface { EGL_NONE}; } EGLSurface pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); - Assertions.checkState("eglCreatePbufferSurface failed", pbuffer != null); + Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); boolean eglMadeCurrent = eglMakeCurrent(display, pbuffer, pbuffer, context); - Assertions.checkState("eglMakeCurrent failed", eglMadeCurrent); + Assertions.checkState(eglMadeCurrent, "eglMakeCurrent failed"); glGenTextures(1, textureIdHolder, 0); surfaceTexture = new SurfaceTexture(textureIdHolder[0]); From 3b1e4b5ca9329cf161e64a5793261cf61388ed0d Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 23 May 2017 07:45:53 -0700 Subject: [PATCH 058/220] Update gradle + bintray-release ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156860658 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cbc34cecd6..da75def90b 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.0' + classpath 'com.android.tools.build:gradle:2.3.1' classpath 'com.novoda:bintray-release:0.4.0' } // Workaround for the following test coverage issue. Remove when fixed: From 3a448f3a0ea935f9e11b8a6bd5db8b4a62144cf3 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 23 May 2017 09:41:37 -0700 Subject: [PATCH 059/220] Bump version and update release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156872383 --- RELEASENOTES.md | 23 +++++++++++++++++++ build.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 ++-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 ++--- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d216e767b0..6a1defa809 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,28 @@ # Release notes # +### r2.4.1 ### + +* Stability: Avoid OutOfMemoryError in extractors when parsing malformed media + ([#2780](https://github.com/google/ExoPlayer/issues/2780)). +* Stability: Avoid native crash on Galaxy Nexus. Avoid unnecessarily large codec + input buffer allocations on all devices + ([#2607](https://github.com/google/ExoPlayer/issues/2607)). +* Variable speed playback: Fix interpolation for rate/pitch adjustment + ([#2774](https://github.com/google/ExoPlayer/issues/2774)). +* HLS: Include EXT-X-DATERANGE tags in HlsMediaPlaylist. +* HLS: Don't expose CEA-608 track if CLOSED-CAPTIONS=NONE + ([#2743](https://github.com/google/ExoPlayer/issues/2743)). +* HLS: Correctly propagate errors loading the media playlist + ([#2623](https://github.com/google/ExoPlayer/issues/2623)). +* UI: DefaultTimeBar enhancements and bug fixes + ([#2740](https://github.com/google/ExoPlayer/issues/2740)). +* Ogg: Fix failure to play some Ogg files + ([#2782](https://github.com/google/ExoPlayer/issues/2782)). +* Captions: Don't select text tack with no language by default. +* Captions: TTML positioning fixes + ([#2824](https://github.com/google/ExoPlayer/issues/2824)). +* Misc bugfixes. + ### r2.4.0 ### * New modular library structure. You can read more about depending on individual diff --git a/build.gradle b/build.gradle index da75def90b..258b11d2e6 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ allprojects { releaseRepoName = getBintrayRepo() releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.4.0' + releaseVersion = 'r2.4.1' releaseWebsite = 'https://github.com/google/ExoPlayer' } if (it.hasProperty('externalBuildDir')) { diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 6580e687cc..1bb859028d 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2401" + android:versionName="2.4.1"> 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 13cf35d449..23c2ddbde9 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 @@ -24,13 +24,13 @@ public interface 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. - String VERSION = "2.4.0"; + String VERSION = "2.4.1"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - String VERSION_SLASHY = "ExoPlayerLib/2.4.0"; + String VERSION_SLASHY = "ExoPlayerLib/2.4.1"; /** * The version of the library expressed as an integer, for example 1002003. @@ -40,7 +40,7 @@ public interface ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - int VERSION_INT = 2004000; + int VERSION_INT = 2004001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 695347c26b92edcbb225eb6d85ca4713362d11ce Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 24 May 2017 07:38:42 -0700 Subject: [PATCH 060/220] Don't select more than one audio/video/text track by default Issue: #2618 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156986606 --- .../trackselection/DefaultTrackSelector.java | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 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 361fcf0b57..b37088e588 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 @@ -436,35 +436,48 @@ public class DefaultTrackSelector extends MappingTrackSelector { int rendererCount = rendererCapabilities.length; TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCount]; Parameters params = paramsReference.get(); - boolean videoTrackAndRendererPresent = false; + boolean seenVideoRendererWithMappedTracks = false; + boolean selectedVideoTracks = false; for (int i = 0; i < rendererCount; i++) { if (C.TRACK_TYPE_VIDEO == rendererCapabilities[i].getTrackType()) { - rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i], - rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth, - params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness, - params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight, - params.orientationMayChange, adaptiveTrackSelectionFactory, - params.exceedVideoConstraintsIfNecessary, params.exceedRendererCapabilitiesIfNecessary); - videoTrackAndRendererPresent |= rendererTrackGroupArrays[i].length > 0; + if (!selectedVideoTracks) { + rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i], + rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth, + params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness, + params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight, + params.orientationMayChange, adaptiveTrackSelectionFactory, + params.exceedVideoConstraintsIfNecessary, + params.exceedRendererCapabilitiesIfNecessary); + selectedVideoTracks = rendererTrackSelections[i] != null; + } + seenVideoRendererWithMappedTracks |= rendererTrackGroupArrays[i].length > 0; } } + boolean selectedAudioTracks = false; + boolean selectedTextTracks = false; for (int i = 0; i < rendererCount; i++) { switch (rendererCapabilities[i].getTrackType()) { case C.TRACK_TYPE_VIDEO: // Already done. Do nothing. break; case C.TRACK_TYPE_AUDIO: - rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i], - rendererFormatSupports[i], params.preferredAudioLanguage, - params.exceedRendererCapabilitiesIfNecessary, params.allowMixedMimeAdaptiveness, - videoTrackAndRendererPresent ? null : adaptiveTrackSelectionFactory); + if (!selectedAudioTracks) { + rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i], + rendererFormatSupports[i], params.preferredAudioLanguage, + params.exceedRendererCapabilitiesIfNecessary, params.allowMixedMimeAdaptiveness, + seenVideoRendererWithMappedTracks ? null : adaptiveTrackSelectionFactory); + selectedAudioTracks = rendererTrackSelections[i] != null; + } break; case C.TRACK_TYPE_TEXT: - rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i], - rendererFormatSupports[i], params.preferredTextLanguage, - params.preferredAudioLanguage, params.exceedRendererCapabilitiesIfNecessary); + if (!selectedTextTracks) { + rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i], + rendererFormatSupports[i], params.preferredTextLanguage, + params.preferredAudioLanguage, params.exceedRendererCapabilitiesIfNecessary); + selectedTextTracks = rendererTrackSelections[i] != null; + } break; default: rendererTrackSelections[i] = selectOtherTrack(rendererCapabilities[i].getTrackType(), From f16967cdfeda442128edc754cba0c40949a2d429 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 24 May 2017 09:48:39 -0700 Subject: [PATCH 061/220] Flexibilize Util.parseXsDateTime to allow single digit hour ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156999955 --- .../java/com/google/android/exoplayer2/util/UtilTest.java | 1 + .../src/main/java/com/google/android/exoplayer2/util/Util.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java index 923d1d8aaa..1d9aff0723 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java @@ -146,6 +146,7 @@ public class UtilTest extends TestCase { assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-08:00")); assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-0800")); assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55.000-0800")); + assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55.000-800")); } public void testUnescapeInvalidFileName() { 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 206349fa07..50932cdf48 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 @@ -98,7 +98,7 @@ public final class Util { private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile( "(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]" + "(\\d\\d):(\\d\\d):(\\d\\d)([\\.,](\\d+))?" - + "([Zz]|((\\+|\\-)(\\d\\d):?(\\d\\d)))?"); + + "([Zz]|((\\+|\\-)(\\d?\\d):?(\\d\\d)))?"); private static final Pattern XS_DURATION_PATTERN = Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?" + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); From eb3a31c881d5c5426c11a28047949669ab664758 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 25 May 2017 03:57:11 -0700 Subject: [PATCH 062/220] Fix default position masking period index calculation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157095116 --- .../java/com/google/android/exoplayer2/ExoPlayerImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 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 94c43167d8..11b1a46cf8 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 @@ -213,10 +213,10 @@ import java.util.concurrent.CopyOnWriteArraySet; maskingPeriodIndex = 0; } else { timeline.getWindow(windowIndex, window); - long resolvedPositionMs = - positionMs == C.TIME_UNSET ? window.getDefaultPositionUs() : positionMs; + long resolvedPositionUs = + positionMs == C.TIME_UNSET ? window.getDefaultPositionUs() : C.msToUs(positionMs); int periodIndex = window.firstPeriodIndex; - long periodPositionUs = window.getPositionInFirstPeriodUs() + C.msToUs(resolvedPositionMs); + long periodPositionUs = window.getPositionInFirstPeriodUs() + resolvedPositionUs; long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs(); while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs && periodIndex < window.lastPeriodIndex) { From c4f7a2d62da280370dee61e102f0fe4eb74cc258 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 25 May 2017 06:17:04 -0700 Subject: [PATCH 063/220] Correctly transition to ended state This fixes transitioning into the ended state if we see endOfStream from the chunk source whilst in the pending reset state. Prior to this fix we'd still be pending a reset, and so readData would never allow EOS to be read by the consuming renderer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157101755 --- .../source/chunk/ChunkSampleStream.java | 25 +++++++++++-------- .../source/dash/DashMediaSource.java | 6 ++--- .../source/hls/HlsSampleStreamWrapper.java | 23 +++++++++-------- 3 files changed, 30 insertions(+), 24 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 c43f3d577a..8f32eb46b8 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 @@ -336,6 +336,7 @@ public class ChunkSampleStream implements SampleStream, S nextChunkHolder.clear(); if (endOfStream) { + pendingResetPositionUs = C.TIME_UNSET; loadingFinished = true; return true; } @@ -389,18 +390,20 @@ public class ChunkSampleStream implements SampleStream, S } private void discardDownstreamMediaChunks(int primaryStreamReadIndex) { - while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex(0) <= primaryStreamReadIndex) { - mediaChunks.removeFirst(); + if (!mediaChunks.isEmpty()) { + while (mediaChunks.size() > 1 + && mediaChunks.get(1).getFirstSampleIndex(0) <= primaryStreamReadIndex) { + mediaChunks.removeFirst(); + } + BaseMediaChunk currentChunk = mediaChunks.getFirst(); + Format trackFormat = currentChunk.trackFormat; + if (!trackFormat.equals(primaryDownstreamTrackFormat)) { + eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat, + currentChunk.trackSelectionReason, currentChunk.trackSelectionData, + currentChunk.startTimeUs); + } + primaryDownstreamTrackFormat = trackFormat; } - BaseMediaChunk currentChunk = mediaChunks.getFirst(); - Format trackFormat = currentChunk.trackFormat; - if (!trackFormat.equals(primaryDownstreamTrackFormat)) { - eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat, - currentChunk.trackSelectionReason, currentChunk.trackSelectionData, - currentChunk.startTimeUs); - } - primaryDownstreamTrackFormat = trackFormat; } /** 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 5ab04ea7be..a469f0aae8 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 @@ -625,9 +625,9 @@ public final class DashMediaSource implements MediaSource { private final long windowDefaultStartPositionUs; private final DashManifest manifest; - public DashTimeline(long presentationStartTimeMs, long windowStartTimeMs, - int firstPeriodId, long offsetInFirstPeriodUs, long windowDurationUs, - long windowDefaultStartPositionUs, DashManifest manifest) { + public DashTimeline(long presentationStartTimeMs, long windowStartTimeMs, int firstPeriodId, + long offsetInFirstPeriodUs, long windowDurationUs, long windowDefaultStartPositionUs, + DashManifest manifest) { this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; this.firstPeriodId = firstPeriodId; 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 827a6e885d..367c43caf1 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 @@ -296,17 +296,19 @@ import java.util.LinkedList; return C.RESULT_NOTHING_READ; } - while (mediaChunks.size() > 1 && finishedReadingChunk(mediaChunks.getFirst())) { - mediaChunks.removeFirst(); + if (!mediaChunks.isEmpty()) { + while (mediaChunks.size() > 1 && finishedReadingChunk(mediaChunks.getFirst())) { + mediaChunks.removeFirst(); + } + HlsMediaChunk currentChunk = mediaChunks.getFirst(); + Format trackFormat = currentChunk.trackFormat; + if (!trackFormat.equals(downstreamTrackFormat)) { + eventDispatcher.downstreamFormatChanged(trackType, trackFormat, + currentChunk.trackSelectionReason, currentChunk.trackSelectionData, + currentChunk.startTimeUs); + } + downstreamTrackFormat = trackFormat; } - HlsMediaChunk currentChunk = mediaChunks.getFirst(); - Format trackFormat = currentChunk.trackFormat; - if (!trackFormat.equals(downstreamTrackFormat)) { - eventDispatcher.downstreamFormatChanged(trackType, trackFormat, - currentChunk.trackSelectionReason, currentChunk.trackSelectionData, - currentChunk.startTimeUs); - } - downstreamTrackFormat = trackFormat; return sampleQueues.valueAt(group).readData(formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs); @@ -348,6 +350,7 @@ import java.util.LinkedList; nextChunkHolder.clear(); if (endOfStream) { + pendingResetPositionUs = C.TIME_UNSET; loadingFinished = true; return true; } From f8cbe53f20325dd06e920c0d989b891b760bf276 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 25 May 2017 06:52:39 -0700 Subject: [PATCH 064/220] Fix SmoothStreaming Timeline There were a few things wrong. Specifically the case in the ref'd issue. Also, the timeline was being marked as non-dynamic in the empty-but-live case (it should be marked dynamic as segments may be added later). Issue: #2760 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157103727 --- .../source/smoothstreaming/SsMediaSource.java | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) 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 e3fb8b606c..d16620d5b2 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 @@ -287,39 +287,41 @@ public final class SsMediaSource implements MediaSource, for (int i = 0; i < mediaPeriods.size(); i++) { mediaPeriods.get(i).updateManifest(manifest); } + + long startTimeUs = Long.MAX_VALUE; + long endTimeUs = Long.MIN_VALUE; + for (StreamElement element : manifest.streamElements) { + if (element.chunkCount > 0) { + startTimeUs = Math.min(startTimeUs, element.getStartTimeUs(0)); + endTimeUs = Math.max(endTimeUs, element.getStartTimeUs(element.chunkCount - 1) + + element.getChunkDurationUs(element.chunkCount - 1)); + } + } + Timeline timeline; - if (manifest.isLive) { - long startTimeUs = Long.MAX_VALUE; - long endTimeUs = Long.MIN_VALUE; - for (int i = 0; i < manifest.streamElements.length; i++) { - StreamElement element = manifest.streamElements[i]; - if (element.chunkCount > 0) { - startTimeUs = Math.min(startTimeUs, element.getStartTimeUs(0)); - endTimeUs = Math.max(endTimeUs, element.getStartTimeUs(element.chunkCount - 1) - + element.getChunkDurationUs(element.chunkCount - 1)); - } + if (startTimeUs == Long.MAX_VALUE) { + long periodDurationUs = manifest.isLive ? C.TIME_UNSET : 0; + timeline = new SinglePeriodTimeline(periodDurationUs, 0, 0, 0, true /* isSeekable */, + manifest.isLive /* isDynamic */); + } else if (manifest.isLive) { + if (manifest.dvrWindowLengthUs != C.TIME_UNSET && manifest.dvrWindowLengthUs > 0) { + startTimeUs = Math.max(startTimeUs, endTimeUs - manifest.dvrWindowLengthUs); } - if (startTimeUs == Long.MAX_VALUE) { - timeline = new SinglePeriodTimeline(C.TIME_UNSET, false); - } else { - if (manifest.dvrWindowLengthUs != C.TIME_UNSET - && manifest.dvrWindowLengthUs > 0) { - startTimeUs = Math.max(startTimeUs, endTimeUs - manifest.dvrWindowLengthUs); - } - long durationUs = endTimeUs - startTimeUs; - long defaultStartPositionUs = durationUs - C.msToUs(livePresentationDelayMs); - if (defaultStartPositionUs < MIN_LIVE_DEFAULT_START_POSITION_US) { - // The default start position is too close to the start of the live window. Set it to the - // minimum default start position provided the window is at least twice as big. Else set - // it to the middle of the window. - defaultStartPositionUs = Math.min(MIN_LIVE_DEFAULT_START_POSITION_US, durationUs / 2); - } - timeline = new SinglePeriodTimeline(C.TIME_UNSET, durationUs, startTimeUs, - defaultStartPositionUs, true /* isSeekable */, true /* isDynamic */); + long durationUs = endTimeUs - startTimeUs; + long defaultStartPositionUs = durationUs - C.msToUs(livePresentationDelayMs); + if (defaultStartPositionUs < MIN_LIVE_DEFAULT_START_POSITION_US) { + // The default start position is too close to the start of the live window. Set it to the + // minimum default start position provided the window is at least twice as big. Else set + // it to the middle of the window. + defaultStartPositionUs = Math.min(MIN_LIVE_DEFAULT_START_POSITION_US, durationUs / 2); } + timeline = new SinglePeriodTimeline(C.TIME_UNSET, durationUs, startTimeUs, + defaultStartPositionUs, true /* isSeekable */, true /* isDynamic */); } else { - boolean isSeekable = manifest.durationUs != C.TIME_UNSET; - timeline = new SinglePeriodTimeline(manifest.durationUs, isSeekable); + long durationUs = manifest.durationUs != C.TIME_UNSET ? manifest.durationUs + : endTimeUs - startTimeUs; + timeline = new SinglePeriodTimeline(startTimeUs + durationUs, durationUs, startTimeUs, 0, + true /* isSeekable */, false /* isDynamic */); } sourceListener.onSourceInfoRefreshed(timeline, manifest); } From 3daa7a084fe89dc241768403f61086694388a249 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 25 May 2017 10:30:27 -0700 Subject: [PATCH 065/220] Shorten the player type The string is truncated to 20 character in IMA. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157122929 --- .../com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 0c89ff604c..c4b626e355 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 @@ -111,8 +111,7 @@ import java.util.List; */ private static final boolean ENABLE_PRELOADING = true; - private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = - "google/com.google.android.exoplayer2.ext.ima"; + private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima"; private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; /** From 5092efe301210f53ff69c475543f02c95b0c254f Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 26 May 2017 03:03:48 -0700 Subject: [PATCH 066/220] Add a comment to record the reason for restoring licenses before releasing ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157204373 --- .../google/android/exoplayer2/drm/DefaultDrmSessionManager.java | 2 ++ 1 file changed, 2 insertions(+) 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 6fc149ba32..cee174adbd 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 @@ -513,6 +513,8 @@ public class DefaultDrmSessionManager implements DrmSe } break; case MODE_RELEASE: + // 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()) { postKeyRequest(offlineLicenseKeySetId, MediaDrm.KEY_TYPE_RELEASE); } From 9737046e53e1db976e77bfaa53e7901170b14e38 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 May 2017 04:48:11 -0700 Subject: [PATCH 067/220] Parse SupplementalProperty elements in DASH adaptation sets This will be used to merge adaptation sets that are marked for seamless switching into single TrackGroups in DashMediaSource. Issue: #2431 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157209358 --- .../exoplayer2/source/dash/DashUtilTest.java | 2 +- .../dash/manifest/DashManifestParserTest.java | 8 +- .../dash/manifest/DashManifestTest.java | 5 +- .../source/dash/DashMediaPeriod.java | 6 +- .../source/dash/manifest/AdaptationSet.java | 23 ++++-- .../source/dash/manifest/DashManifest.java | 3 +- .../dash/manifest/DashManifestParser.java | 81 +++++++------------ .../{SchemeValuePair.java => Descriptor.java} | 39 ++++++--- .../source/dash/manifest/Representation.java | 19 +++-- 9 files changed, 97 insertions(+), 89 deletions(-) rename library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/{SchemeValuePair.java => Descriptor.java} (52%) diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java index c9f1ca1030..d714573a82 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java @@ -62,7 +62,7 @@ public final class DashUtilTest extends TestCase { } private static AdaptationSet newAdaptationSets(Representation... representations) { - return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), null); + return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), null, null); } private static Representation newRepresentations(DrmInitData drmInitData) { diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index 5b8760f929..3ce4b37ec6 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -115,12 +115,12 @@ public class DashManifestParserTest extends InstrumentationTestCase { buildCea708AccessibilityDescriptors("Wrong format"))); } - private static List buildCea608AccessibilityDescriptors(String value) { - return Collections.singletonList(new SchemeValuePair("urn:scte:dash:cc:cea-608:2015", value)); + private static List buildCea608AccessibilityDescriptors(String value) { + return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-608:2015", value, null)); } - private static List buildCea708AccessibilityDescriptors(String value) { - return Collections.singletonList(new SchemeValuePair("urn:scte:dash:cc:cea-708:2015", value)); + private static List buildCea708AccessibilityDescriptors(String value) { + return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-708:2015", value, null)); } } diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java index c796025b08..7d77ae82d9 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java @@ -30,8 +30,6 @@ import junit.framework.TestCase; public class DashManifestTest extends TestCase { private static final UtcTimingElement DUMMY_UTC_TIMING = new UtcTimingElement("", ""); - private static final List DUMMY_ACCESSIBILITY_DESCRIPTORS = - Collections.emptyList(); private static final SingleSegmentBase DUMMY_SEGMENT_BASE = new SingleSegmentBase(); private static final Format DUMMY_FORMAT = Format.createSampleFormat("", "", 0); @@ -190,8 +188,7 @@ public class DashManifestTest extends TestCase { } private static AdaptationSet newAdaptationSet(int seed, Representation... representations) { - return new AdaptationSet(++seed, ++seed, Arrays.asList(representations), - DUMMY_ACCESSIBILITY_DESCRIPTORS); + return new AdaptationSet(++seed, ++seed, Arrays.asList(representations), null, null); } } 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 5e0541cb31..56fc532f9b 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 @@ -30,8 +30,8 @@ import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; import com.google.android.exoplayer2.source.chunk.ChunkSampleStream.EmbeddedSampleStream; 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.Representation; -import com.google.android.exoplayer2.source.dash.manifest.SchemeValuePair; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; @@ -319,9 +319,9 @@ import java.util.List; } private static boolean hasCea608Track(AdaptationSet adaptationSet) { - List descriptors = adaptationSet.accessibilityDescriptors; + List descriptors = adaptationSet.accessibilityDescriptors; for (int i = 0; i < descriptors.size(); i++) { - SchemeValuePair descriptor = descriptors.get(i); + Descriptor descriptor = descriptors.get(i); if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)) { return true; } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java index 097676b89f..fd91a2f784 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java @@ -41,31 +41,40 @@ public class AdaptationSet { public final int type; /** - * The {@link Representation}s in the adaptation set. + * {@link Representation}s in the adaptation set. */ public final List representations; /** - * The accessibility descriptors in the adaptation set. + * Accessibility descriptors in the adaptation set. */ - public final List accessibilityDescriptors; + public final List accessibilityDescriptors; + + /** + * Supplemental properties in the adaptation set. + */ + public final List supplementalProperties; /** * @param id A non-negative identifier for the adaptation set that's unique in the scope of its * containing period, or {@link #ID_UNSET} if not specified. * @param type The type of the adaptation set. One of the {@link com.google.android.exoplayer2.C} * {@code TRACK_TYPE_*} constants. - * @param representations The {@link Representation}s in the adaptation set. - * @param accessibilityDescriptors The accessibility descriptors in the adaptation set. + * @param representations {@link Representation}s in the adaptation set. + * @param accessibilityDescriptors Accessibility descriptors in the adaptation set. + * @param supplementalProperties Supplemental properties in the adaptation set. */ public AdaptationSet(int id, int type, List representations, - List accessibilityDescriptors) { + List accessibilityDescriptors, List supplementalProperties) { this.id = id; this.type = type; this.representations = Collections.unmodifiableList(representations); this.accessibilityDescriptors = accessibilityDescriptors == null - ? Collections.emptyList() + ? Collections.emptyList() : Collections.unmodifiableList(accessibilityDescriptors); + this.supplementalProperties = supplementalProperties == null + ? Collections.emptyList() + : Collections.unmodifiableList(supplementalProperties); } } 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 eb51c8312d..cd02e27fce 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 @@ -134,7 +134,8 @@ public class DashManifest { } while(key.periodIndex == periodIndex && key.adaptationSetIndex == adaptationSetIndex); copyAdaptationSets.add(new AdaptationSet(adaptationSet.id, adaptationSet.type, - copyRepresentations, adaptationSet.accessibilityDescriptors)); + copyRepresentations, adaptationSet.accessibilityDescriptors, + adaptationSet.supplementalProperties)); } while(key.periodIndex == periodIndex); // Add back the last key which doesn't belong to the period being processed keys.addFirst(key); 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 d4338fd812..0682af5dd6 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 @@ -239,8 +239,9 @@ public class DashManifestParser extends DefaultHandler int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE); String language = xpp.getAttributeValue(null, "lang"); ArrayList drmSchemeDatas = new ArrayList<>(); - ArrayList inbandEventStreams = new ArrayList<>(); - ArrayList accessibilityDescriptors = new ArrayList<>(); + ArrayList inbandEventStreams = new ArrayList<>(); + ArrayList accessibilityDescriptors = new ArrayList<>(); + ArrayList supplementalProperties = new ArrayList<>(); List representationInfos = new ArrayList<>(); @C.SelectionFlags int selectionFlags = 0; @@ -265,7 +266,9 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) { audioChannels = parseAudioChannelConfiguration(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "Accessibility")) { - accessibilityDescriptors.add(parseAccessibility(xpp)); + accessibilityDescriptors.add(parseDescriptor(xpp, "Accessibility")); + } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { + supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); } else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) { RepresentationInfo representationInfo = parseRepresentation(xpp, baseUrl, mimeType, codecs, width, height, frameRate, audioChannels, audioSamplingRate, language, @@ -280,7 +283,7 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { - inbandEventStreams.add(parseInbandEventStream(xpp)); + inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); } else if (XmlPullParserUtil.isStartTag(xpp)) { parseAdaptationSetChild(xpp); } @@ -293,12 +296,15 @@ public class DashManifestParser extends DefaultHandler drmSchemeDatas, inbandEventStreams)); } - return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors); + return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors, + supplementalProperties); } protected AdaptationSet buildAdaptationSet(int id, int contentType, - List representations, List accessibilityDescriptors) { - return new AdaptationSet(id, contentType, representations, accessibilityDescriptors); + List representations, List accessibilityDescriptors, + List supplementalProperties) { + return new AdaptationSet(id, contentType, representations, accessibilityDescriptors, + supplementalProperties); } protected int parseContentType(XmlPullParser xpp) { @@ -366,32 +372,6 @@ public class DashManifestParser extends DefaultHandler : null; } - /** - * Parses an InbandEventStream element. - * - * @param xpp The parser from which to read. - * @throws XmlPullParserException If an error occurs parsing the element. - * @throws IOException If an error occurs reading the element. - * @return A {@link SchemeValuePair} parsed from the element. - */ - protected SchemeValuePair parseInbandEventStream(XmlPullParser xpp) - throws XmlPullParserException, IOException { - return parseSchemeValuePair(xpp, "InbandEventStream"); - } - - /** - * Parses an Accessibility element. - * - * @param xpp The parser from which to read. - * @throws XmlPullParserException If an error occurs parsing the element. - * @throws IOException If an error occurs reading the element. - * @return A {@link SchemeValuePair} parsed from the element. - */ - protected SchemeValuePair parseAccessibility(XmlPullParser xpp) - throws XmlPullParserException, IOException { - return parseSchemeValuePair(xpp, "Accessibility"); - } - /** * Parses a Role element. * @@ -429,7 +409,7 @@ public class DashManifestParser extends DefaultHandler int adaptationSetHeight, float adaptationSetFrameRate, int adaptationSetAudioChannels, int adaptationSetAudioSamplingRate, String adaptationSetLanguage, @C.SelectionFlags int adaptationSetSelectionFlags, - List adaptationSetAccessibilityDescriptors, SegmentBase segmentBase) + List adaptationSetAccessibilityDescriptors, SegmentBase segmentBase) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE); @@ -442,7 +422,7 @@ public class DashManifestParser extends DefaultHandler int audioChannels = adaptationSetAudioChannels; int audioSamplingRate = parseInt(xpp, "audioSamplingRate", adaptationSetAudioSamplingRate); ArrayList drmSchemeDatas = new ArrayList<>(); - ArrayList inbandEventStreams = new ArrayList<>(); + ArrayList inbandEventStreams = new ArrayList<>(); boolean seenFirstBaseUrl = false; do { @@ -466,7 +446,7 @@ public class DashManifestParser extends DefaultHandler drmSchemeDatas.add(contentProtection); } } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { - inbandEventStreams.add(parseInbandEventStream(xpp)); + inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); } } while (!XmlPullParserUtil.isEndTag(xpp, "Representation")); @@ -480,7 +460,7 @@ public class DashManifestParser extends DefaultHandler protected Format buildFormat(String id, String containerMimeType, int width, int height, float frameRate, int audioChannels, int audioSamplingRate, int bitrate, String language, - @C.SelectionFlags int selectionFlags, List accessibilityDescriptors, + @C.SelectionFlags int selectionFlags, List accessibilityDescriptors, String codecs) { String sampleMimeType = getSampleMimeType(containerMimeType, codecs); if (sampleMimeType != null) { @@ -509,14 +489,14 @@ public class DashManifestParser extends DefaultHandler protected Representation buildRepresentation(RepresentationInfo representationInfo, String contentId, ArrayList extraDrmSchemeDatas, - ArrayList extraInbandEventStreams) { + ArrayList extraInbandEventStreams) { Format format = representationInfo.format; ArrayList drmSchemeDatas = representationInfo.drmSchemeDatas; drmSchemeDatas.addAll(extraDrmSchemeDatas); if (!drmSchemeDatas.isEmpty()) { format = format.copyWithDrmInitData(new DrmInitData(drmSchemeDatas)); } - ArrayList inbandEventStremas = representationInfo.inbandEventStreams; + ArrayList inbandEventStremas = representationInfo.inbandEventStreams; inbandEventStremas.addAll(extraInbandEventStreams); return Representation.newInstance(contentId, Representation.REVISION_ID_DEFAULT, format, representationInfo.baseUrl, representationInfo.segmentBase, inbandEventStremas); @@ -809,28 +789,29 @@ public class DashManifestParser extends DefaultHandler } /** - * Parses a {@link SchemeValuePair} from an element. + * Parses a {@link Descriptor} from an element. * * @param xpp The parser from which to read. * @param tag The tag of the element being parsed. * @throws XmlPullParserException If an error occurs parsing the element. * @throws IOException If an error occurs reading the element. - * @return The parsed {@link SchemeValuePair}. + * @return The parsed {@link Descriptor}. */ - protected static SchemeValuePair parseSchemeValuePair(XmlPullParser xpp, String tag) + protected static Descriptor parseDescriptor(XmlPullParser xpp, String tag) throws XmlPullParserException, IOException { - String schemeIdUri = parseString(xpp, "schemeIdUri", null); + String schemeIdUri = parseString(xpp, "schemeIdUri", ""); String value = parseString(xpp, "value", null); + String id = parseString(xpp, "id", null); do { xpp.next(); } while (!XmlPullParserUtil.isEndTag(xpp, tag)); - return new SchemeValuePair(schemeIdUri, value); + return new Descriptor(schemeIdUri, value, id); } protected static int parseCea608AccessibilityChannel( - List accessibilityDescriptors) { + List accessibilityDescriptors) { for (int i = 0; i < accessibilityDescriptors.size(); i++) { - SchemeValuePair descriptor = accessibilityDescriptors.get(i); + Descriptor descriptor = accessibilityDescriptors.get(i); if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri) && descriptor.value != null) { Matcher accessibilityValueMatcher = CEA_608_ACCESSIBILITY_PATTERN.matcher(descriptor.value); @@ -845,9 +826,9 @@ public class DashManifestParser extends DefaultHandler } protected static int parseCea708AccessibilityChannel( - List accessibilityDescriptors) { + List accessibilityDescriptors) { for (int i = 0; i < accessibilityDescriptors.size(); i++) { - SchemeValuePair descriptor = accessibilityDescriptors.get(i); + Descriptor descriptor = accessibilityDescriptors.get(i); if ("urn:scte:dash:cc:cea-708:2015".equals(descriptor.schemeIdUri) && descriptor.value != null) { Matcher accessibilityValueMatcher = CEA_708_ACCESSIBILITY_PATTERN.matcher(descriptor.value); @@ -925,10 +906,10 @@ public class DashManifestParser extends DefaultHandler public final String baseUrl; public final SegmentBase segmentBase; public final ArrayList drmSchemeDatas; - public final ArrayList inbandEventStreams; + public final ArrayList inbandEventStreams; public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase, - ArrayList drmSchemeDatas, ArrayList inbandEventStreams) { + ArrayList drmSchemeDatas, ArrayList inbandEventStreams) { this.format = format; this.baseUrl = baseUrl; this.segmentBase = segmentBase; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SchemeValuePair.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java similarity index 52% rename from library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SchemeValuePair.java rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java index 470bf0f989..18d0a937ab 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SchemeValuePair.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java @@ -15,19 +15,37 @@ */ package com.google.android.exoplayer2.source.dash.manifest; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Util; /** - * A pair consisting of a scheme ID and value. + * A descriptor, as defined by ISO 23009-1, 2nd edition, 5.8.2. */ -public class SchemeValuePair { +public final class Descriptor { - public final String schemeIdUri; - public final String value; + /** + * The scheme URI. + */ + @NonNull public final String schemeIdUri; + /** + * The value, or null. + */ + @Nullable public final String value; + /** + * The identifier, or null. + */ + @Nullable public final String id; - public SchemeValuePair(String schemeIdUri, String value) { + /** + * @param schemeIdUri The scheme URI. + * @param value The value, or null. + * @param id The identifier, or null. + */ + public Descriptor(@NonNull String schemeIdUri, @Nullable String value, @Nullable String id) { this.schemeIdUri = schemeIdUri; this.value = value; + this.id = id; } @Override @@ -38,14 +56,17 @@ public class SchemeValuePair { if (obj == null || getClass() != obj.getClass()) { return false; } - SchemeValuePair other = (SchemeValuePair) obj; - return Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value); + Descriptor other = (Descriptor) obj; + return Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value) + && Util.areEqual(id, other.id); } @Override public int hashCode() { - return 31 * (schemeIdUri != null ? schemeIdUri.hashCode() : 0) - + (value != null ? value.hashCode() : 0); + int result = (schemeIdUri != null ? schemeIdUri.hashCode() : 0); + 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/Representation.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java index 5960d4d7ba..81e4602c1d 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 @@ -65,7 +65,7 @@ public abstract class Representation { /** * The in-band event streams in the representation. Never null, but may be empty. */ - public final List inbandEventStreams; + public final List inbandEventStreams; private final RangedUri initializationUri; @@ -96,7 +96,7 @@ public abstract class Representation { * @return The constructed instance. */ public static Representation newInstance(String contentId, long revisionId, Format format, - String baseUrl, SegmentBase segmentBase, List inbandEventStreams) { + String baseUrl, SegmentBase segmentBase, List inbandEventStreams) { return newInstance(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams, null); } @@ -115,7 +115,7 @@ public abstract class Representation { * @return The constructed instance. */ public static Representation newInstance(String contentId, long revisionId, Format format, - String baseUrl, SegmentBase segmentBase, List inbandEventStreams, + String baseUrl, SegmentBase segmentBase, List inbandEventStreams, String customCacheKey) { if (segmentBase instanceof SingleSegmentBase) { return new SingleSegmentRepresentation(contentId, revisionId, format, baseUrl, @@ -130,13 +130,12 @@ public abstract class Representation { } private Representation(String contentId, long revisionId, Format format, String baseUrl, - SegmentBase segmentBase, List inbandEventStreams) { + SegmentBase segmentBase, List inbandEventStreams) { this.contentId = contentId; this.revisionId = revisionId; this.format = format; this.baseUrl = baseUrl; - this.inbandEventStreams = inbandEventStreams == null - ? Collections.emptyList() + this.inbandEventStreams = inbandEventStreams == null ? Collections.emptyList() : Collections.unmodifiableList(inbandEventStreams); initializationUri = segmentBase.getInitialization(this); presentationTimeOffsetUs = segmentBase.getPresentationTimeOffsetUs(); @@ -201,8 +200,8 @@ public abstract class Representation { */ public static SingleSegmentRepresentation newInstance(String contentId, long revisionId, Format format, String uri, long initializationStart, long initializationEnd, - long indexStart, long indexEnd, List inbandEventStreams, - String customCacheKey, long contentLength) { + long indexStart, long indexEnd, List inbandEventStreams, String customCacheKey, + long contentLength) { RangedUri rangedUri = new RangedUri(null, initializationStart, initializationEnd - initializationStart + 1); SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, indexStart, @@ -222,7 +221,7 @@ public abstract class Representation { * @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown. */ public SingleSegmentRepresentation(String contentId, long revisionId, Format format, - String baseUrl, SingleSegmentBase segmentBase, List inbandEventStreams, + String baseUrl, SingleSegmentBase segmentBase, List inbandEventStreams, String customCacheKey, long contentLength) { super(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.uri = Uri.parse(baseUrl); @@ -270,7 +269,7 @@ public abstract class Representation { * @param inbandEventStreams The in-band event streams in the representation. May be null. */ public MultiSegmentRepresentation(String contentId, long revisionId, Format format, - String baseUrl, MultiSegmentBase segmentBase, List inbandEventStreams) { + String baseUrl, MultiSegmentBase segmentBase, List inbandEventStreams) { super(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.segmentBase = segmentBase; } From 2c20689237353472cad859335c488fa42ae83231 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 May 2017 04:49:44 -0700 Subject: [PATCH 068/220] Don't fail if we find a track is unsupported Use AUDIO_UNKNOWN instead. This is in line with our handling of video tracks with VIDEO_UNKNOWN. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157209428 --- .../extractor/mkv/MatroskaExtractor.java | 32 ++++++++++++++----- .../android/exoplayer2/util/MimeTypes.java | 1 + 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 227cbd6f0c..591c56f525 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.mkv; import android.support.annotation.IntDef; +import android.util.Log; import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -84,6 +85,8 @@ public final class MatroskaExtractor implements Extractor { */ public static final int FLAG_DISABLE_SEEK_FOR_CUES = 1; + private static final String TAG = "MatroskaExtractor"; + private static final int UNSET_ENTRY_ID = -1; private static final int BLOCK_STATE_START = 0; @@ -1559,7 +1562,12 @@ public final class MatroskaExtractor implements Extractor { break; case CODEC_ID_FOURCC: initializationData = parseFourCcVc1Private(new ParsableByteArray(codecPrivate)); - mimeType = initializationData == null ? MimeTypes.VIDEO_UNKNOWN : MimeTypes.VIDEO_VC1; + if (initializationData != null) { + mimeType = MimeTypes.VIDEO_VC1; + } else { + Log.w(TAG, "Unsupported FourCC. Setting mimeType to " + MimeTypes.VIDEO_UNKNOWN); + mimeType = MimeTypes.VIDEO_UNKNOWN; + } break; case CODEC_ID_THEORA: // TODO: This can be set to the real mimeType if/when we work out what initializationData @@ -1615,19 +1623,27 @@ public final class MatroskaExtractor implements Extractor { break; case CODEC_ID_ACM: mimeType = MimeTypes.AUDIO_RAW; - if (!parseMsAcmCodecPrivate(new ParsableByteArray(codecPrivate))) { - throw new ParserException("Non-PCM MS/ACM is unsupported"); - } - pcmEncoding = Util.getPcmEncoding(audioBitDepth); - if (pcmEncoding == C.ENCODING_INVALID) { - throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth); + if (parseMsAcmCodecPrivate(new ParsableByteArray(codecPrivate))) { + pcmEncoding = Util.getPcmEncoding(audioBitDepth); + if (pcmEncoding == C.ENCODING_INVALID) { + pcmEncoding = Format.NO_VALUE; + mimeType = MimeTypes.AUDIO_UNKNOWN; + Log.w(TAG, "Unsupported PCM bit depth: " + audioBitDepth + ". Setting mimeType to " + + mimeType); + } + } else { + mimeType = MimeTypes.AUDIO_UNKNOWN; + Log.w(TAG, "Non-PCM MS/ACM is unsupported. Setting mimeType to " + mimeType); } break; case CODEC_ID_PCM_INT_LIT: mimeType = MimeTypes.AUDIO_RAW; pcmEncoding = Util.getPcmEncoding(audioBitDepth); if (pcmEncoding == C.ENCODING_INVALID) { - throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth); + pcmEncoding = Format.NO_VALUE; + mimeType = MimeTypes.AUDIO_UNKNOWN; + Log.w(TAG, "Unsupported PCM bit depth: " + audioBitDepth + ". Setting mimeType to " + + mimeType); } break; case CODEC_ID_SUBRIP: 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 ea669e6f2a..e227ea1068 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 @@ -61,6 +61,7 @@ public final class MimeTypes { public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb"; public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/x-flac"; public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac"; + public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown"; public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; From 3108b07c90334c69015b7bff045240aa169bb6e7 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 26 May 2017 05:10:04 -0700 Subject: [PATCH 069/220] Minimize overlapping chunks if #EXT-X-INDEPENDENT-SEGMENTS is present It is worth mentioning that the tag can be in the master playlist as well, which means that it applies to all media playlists. There are a few tags with this characteristic. This will be addressed in a later CL. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157210505 --- .../exoplayer2/source/hls/HlsChunkSource.java | 5 +++-- .../source/hls/playlist/HlsMediaPlaylist.java | 15 +++++++++------ .../source/hls/playlist/HlsPlaylistParser.java | 8 ++++++-- 3 files changed, 18 insertions(+), 10 deletions(-) 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 795e2f0eaa..ccd126753c 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 @@ -222,8 +222,9 @@ import java.util.Locale; // Select the chunk. int chunkMediaSequence; if (previous == null || switchingVariant) { - long targetPositionUs = previous == null ? playbackPositionUs : previous.startTimeUs; - if (!mediaPlaylist.hasEndTag && targetPositionUs > mediaPlaylist.getEndTimeUs()) { + long targetPositionUs = previous == null ? playbackPositionUs + : mediaPlaylist.hasIndependentSegmentsTag ? previous.endTimeUs : previous.startTimeUs; + if (!mediaPlaylist.hasEndTag && targetPositionUs >= mediaPlaylist.getEndTimeUs()) { // If the playlist is too old to contain the chunk, we need to refresh it. chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); } else { 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 69b95e6d3d..222a710185 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 @@ -87,6 +87,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public final int mediaSequence; public final int version; public final long targetDurationUs; + public final boolean hasIndependentSegmentsTag; public final boolean hasEndTag; public final boolean hasProgramDateTime; public final Segment initializationSegment; @@ -96,9 +97,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public HlsMediaPlaylist(@PlaylistType int playlistType, String baseUri, long startOffsetUs, long startTimeUs, boolean hasDiscontinuitySequence, int discontinuitySequence, - int mediaSequence, int version, long targetDurationUs, boolean hasEndTag, - boolean hasProgramDateTime, Segment initializationSegment, List segments, - List dateRanges) { + int mediaSequence, int version, long targetDurationUs, boolean hasIndependentSegmentsTag, + boolean hasEndTag, boolean hasProgramDateTime, Segment initializationSegment, + List segments, List dateRanges) { super(baseUri); this.playlistType = playlistType; this.startTimeUs = startTimeUs; @@ -107,6 +108,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { this.mediaSequence = mediaSequence; this.version = version; this.targetDurationUs = targetDurationUs; + this.hasIndependentSegmentsTag = hasIndependentSegmentsTag; this.hasEndTag = hasEndTag; this.hasProgramDateTime = hasProgramDateTime; this.initializationSegment = initializationSegment; @@ -157,8 +159,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist { */ public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) { return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, true, - discontinuitySequence, mediaSequence, version, targetDurationUs, hasEndTag, - hasProgramDateTime, initializationSegment, segments, dateRanges); + discontinuitySequence, mediaSequence, version, targetDurationUs, hasIndependentSegmentsTag, + hasEndTag, hasProgramDateTime, initializationSegment, segments, dateRanges); } /** @@ -173,7 +175,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, hasDiscontinuitySequence, discontinuitySequence, mediaSequence, version, targetDurationUs, - true, hasProgramDateTime, initializationSegment, segments, dateRanges); + hasIndependentSegmentsTag, true, hasProgramDateTime, initializationSegment, segments, + dateRanges); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 664306baff..7eb496eba7 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -52,6 +52,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser segments = new ArrayList<>(); @@ -380,14 +382,16 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Fri, 26 May 2017 07:14:59 -0700 Subject: [PATCH 070/220] Expose all "other" tags that start with #EXT through the playlist "other" includes tags for which there is no existing behavior defined. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157217270 --- .../hls/playlist/HlsMasterPlaylist.java | 13 +-- .../source/hls/playlist/HlsMediaPlaylist.java | 89 ++++++++++++++++--- .../source/hls/playlist/HlsPlaylist.java | 17 +++- .../hls/playlist/HlsPlaylistParser.java | 26 ++++-- 4 files changed, 118 insertions(+), 27 deletions(-) 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 874c865049..b38763f7e8 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 @@ -88,16 +88,18 @@ public final class HlsMasterPlaylist extends HlsPlaylist { public final List muxedCaptionFormats; /** - * @param baseUri The base uri. Used to resolve relative paths. + * @param baseUri See {@link #baseUri}. + * @param tags See {@link #tags}. * @param variants See {@link #variants}. * @param audios See {@link #audios}. * @param subtitles See {@link #subtitles}. * @param muxedAudioFormat See {@link #muxedAudioFormat}. * @param muxedCaptionFormats See {@link #muxedCaptionFormats}. */ - public HlsMasterPlaylist(String baseUri, List variants, List audios, - List subtitles, Format muxedAudioFormat, List muxedCaptionFormats) { - super(baseUri); + public HlsMasterPlaylist(String baseUri, List tags, List variants, + List audios, List subtitles, Format muxedAudioFormat, + List muxedCaptionFormats) { + super(baseUri, tags); this.variants = Collections.unmodifiableList(variants); this.audios = Collections.unmodifiableList(audios); this.subtitles = Collections.unmodifiableList(subtitles); @@ -115,7 +117,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist { public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUrl) { List variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUrl)); List emptyList = Collections.emptyList(); - return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, null); + return new HlsMasterPlaylist(null, Collections.emptyList(), variant, emptyList, + emptyList, null, null); } } 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 222a710185..102fe6ee86 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 @@ -70,7 +70,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } /** - * Type of the playlist as specified by #EXT-X-PLAYLIST-TYPE. + * Type of the playlist as defined by #EXT-X-PLAYLIST-TYPE. */ @Retention(RetentionPolicy.SOURCE) @IntDef({PLAYLIST_TYPE_UNKNOWN, PLAYLIST_TYPE_VOD, PLAYLIST_TYPE_EVENT}) @@ -79,28 +79,86 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public static final int PLAYLIST_TYPE_VOD = 1; public static final int PLAYLIST_TYPE_EVENT = 2; + /** + * The type of the playlist. See {@link PlaylistType}. + */ @PlaylistType public final int playlistType; + /** + * The start offset as defined by #EXT-X-START in microseconds. + */ public final long startOffsetUs; + /** + * The start time of the playlist in playback timebase in microseconds. + */ public final long startTimeUs; + /** + * Whether the playlist contains the #EXT-X-DISCONTINUITY-SEQUENCE tag. + */ public final boolean hasDiscontinuitySequence; + /** + * The discontinuity sequence number. + */ public final int discontinuitySequence; + /** + * The media sequence number as defined by #EXT-X-MEDIA-SEQUENCE. + */ public final int mediaSequence; + /** + * The compatibility version as defined by #EXT-X-VERSION. + */ public final int version; + /** + * The target duration as defined by #EXT-X-TARGETDURATION in microseconds. + */ public final long targetDurationUs; + /** + * Whether the playlist contains the #EXT-X-INDEPENDENT-SEGMENTS tag. + */ public final boolean hasIndependentSegmentsTag; + /** + * Whether the playlist contains the #EXT-X-ENDLIST tag. + */ public final boolean hasEndTag; + /** + * Whether the playlist contains a #EXT-X-PROGRAM-DATE-TIME tag. + */ public final boolean hasProgramDateTime; + /** + * The initialization segment as defined by #EXT-X-MAP. + */ public final Segment initializationSegment; + /** + * The list of segments in the playlist. + */ public final List segments; - public final List dateRanges; + /** + * The total duration of the playlist in microseconds. + */ public final long durationUs; - public HlsMediaPlaylist(@PlaylistType int playlistType, String baseUri, long startOffsetUs, - long startTimeUs, boolean hasDiscontinuitySequence, int discontinuitySequence, - int mediaSequence, int version, long targetDurationUs, boolean hasIndependentSegmentsTag, - boolean hasEndTag, boolean hasProgramDateTime, Segment initializationSegment, - List segments, List dateRanges) { - super(baseUri); + /** + * @param playlistType See {@link #playlistType}. + * @param baseUri See {@link #baseUri}. + * @param tags See {@link #tags}. + * @param startOffsetUs See {@link #startOffsetUs}. + * @param startTimeUs See {@link #startTimeUs}. + * @param hasDiscontinuitySequence See {@link #hasDiscontinuitySequence}. + * @param discontinuitySequence See {@link #discontinuitySequence}. + * @param mediaSequence See {@link #mediaSequence}. + * @param version See {@link #version}. + * @param targetDurationUs See {@link #targetDurationUs}. + * @param hasIndependentSegmentsTag See {@link #hasIndependentSegmentsTag}. + * @param hasEndTag See {@link #hasEndTag}. + * @param hasProgramDateTime See {@link #hasProgramDateTime}. + * @param initializationSegment See {@link #initializationSegment}. + * @param segments See {@link #segments}. + */ + public HlsMediaPlaylist(@PlaylistType int playlistType, String baseUri, List tags, + long startOffsetUs, long startTimeUs, boolean hasDiscontinuitySequence, + int discontinuitySequence, int mediaSequence, int version, long targetDurationUs, + boolean hasIndependentSegmentsTag, boolean hasEndTag, boolean hasProgramDateTime, + Segment initializationSegment, List segments) { + super(baseUri, tags); this.playlistType = playlistType; this.startTimeUs = startTimeUs; this.hasDiscontinuitySequence = hasDiscontinuitySequence; @@ -121,7 +179,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } this.startOffsetUs = startOffsetUs == C.TIME_UNSET ? C.TIME_UNSET : startOffsetUs >= 0 ? startOffsetUs : durationUs + startOffsetUs; - this.dateRanges = Collections.unmodifiableList(dateRanges); } /** @@ -144,6 +201,11 @@ public final class HlsMediaPlaylist extends HlsPlaylist { || (segmentCount == otherSegmentCount && hasEndTag && !other.hasEndTag); } + /** + * Returns the result of adding the duration of the playlist to its start time. + * + * @return The result of adding the duration of the playlist to its start time. + */ public long getEndTimeUs() { return startTimeUs + durationUs; } @@ -158,9 +220,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * @return The playlist. */ public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) { - return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, true, + return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, startTimeUs, true, discontinuitySequence, mediaSequence, version, targetDurationUs, hasIndependentSegmentsTag, - hasEndTag, hasProgramDateTime, initializationSegment, segments, dateRanges); + hasEndTag, hasProgramDateTime, initializationSegment, segments); } /** @@ -173,10 +235,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist { if (this.hasEndTag) { return this; } - return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, + return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, startTimeUs, hasDiscontinuitySequence, discontinuitySequence, mediaSequence, version, targetDurationUs, - hasIndependentSegmentsTag, true, hasProgramDateTime, initializationSegment, segments, - dateRanges); + hasIndependentSegmentsTag, true, hasProgramDateTime, initializationSegment, segments); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java index 7c3d64d701..a490c9477c 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java @@ -15,15 +15,30 @@ */ package com.google.android.exoplayer2.source.hls.playlist; +import java.util.Collections; +import java.util.List; + /** * Represents an HLS playlist. */ public abstract class HlsPlaylist { + /** + * The base uri. Used to resolve relative paths. + */ public final String baseUri; + /** + * The list of tags in the playlist. + */ + public final List tags; - protected HlsPlaylist(String baseUri) { + /** + * @param baseUri See {@link #baseUri}. + * @param tags See {@link #tags}. + */ + protected HlsPlaylist(String baseUri, List tags) { this.baseUri = baseUri; + this.tags = Collections.unmodifiableList(tags); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 7eb496eba7..cd6c0283b5 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -43,6 +43,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variants = new ArrayList<>(); ArrayList audios = new ArrayList<>(); ArrayList subtitles = new ArrayList<>(); + ArrayList tags = new ArrayList<>(); Format muxedAudioFormat = null; List muxedCaptionFormats = null; boolean noClosedCaptions = false; @@ -183,6 +185,12 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser segments = new ArrayList<>(); - List dateRanges = new ArrayList<>(); + List tags = new ArrayList<>(); long segmentDurationUs = 0; boolean hasDiscontinuitySequence = false; @@ -296,6 +304,12 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Fri, 26 May 2017 07:48:43 -0700 Subject: [PATCH 071/220] Use AVERAGE-BANDWIDTH instead of BANDWIDTH when available Also prevent BANDWIDTH's regex from matching the AVERAGE-BANDWIDTH attribute. Issue:#2863 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157219453 --- .../playlist/HlsMasterPlaylistParserTest.java | 27 ++++++++++++++----- .../hls/playlist/HlsPlaylistParser.java | 26 ++++++++++-------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index 912dcb28b2..f835c87466 100644 --- a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -51,6 +51,15 @@ public class HlsMasterPlaylistParserTest extends TestCase { + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n" + "http://example.com/audio-only.m3u8"; + private static final String AVG_BANDWIDTH_MASTER_PLAYLIST = " #EXTM3U \n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + + "http://example.com/low.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1270000," + + "CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" + + "http://example.com/spaces_in_codecs.m3u8\n"; + private static final String PLAYLIST_WITH_INVALID_HEADER = "#EXTMU3\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "http://example.com/low.m3u8\n"; @@ -70,42 +79,48 @@ public class HlsMasterPlaylistParserTest extends TestCase { HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST); List variants = masterPlaylist.variants; - assertNotNull(variants); assertEquals(5, variants.size()); assertNull(masterPlaylist.muxedCaptionFormats); assertEquals(1280000, variants.get(0).format.bitrate); - assertNotNull(variants.get(0).format.codecs); assertEquals("mp4a.40.2,avc1.66.30", variants.get(0).format.codecs); assertEquals(304, variants.get(0).format.width); assertEquals(128, variants.get(0).format.height); assertEquals("http://example.com/low.m3u8", variants.get(0).url); assertEquals(1280000, variants.get(1).format.bitrate); - assertNotNull(variants.get(1).format.codecs); assertEquals("mp4a.40.2 , avc1.66.30 ", variants.get(1).format.codecs); assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url); assertEquals(2560000, variants.get(2).format.bitrate); - assertEquals(null, variants.get(2).format.codecs); + assertNull(variants.get(2).format.codecs); assertEquals(384, variants.get(2).format.width); assertEquals(160, variants.get(2).format.height); assertEquals("http://example.com/mid.m3u8", variants.get(2).url); assertEquals(7680000, variants.get(3).format.bitrate); - assertEquals(null, variants.get(3).format.codecs); + assertNull(variants.get(3).format.codecs); assertEquals(Format.NO_VALUE, variants.get(3).format.width); assertEquals(Format.NO_VALUE, variants.get(3).format.height); assertEquals("http://example.com/hi.m3u8", variants.get(3).url); assertEquals(65000, variants.get(4).format.bitrate); - assertNotNull(variants.get(4).format.codecs); assertEquals("mp4a.40.5", variants.get(4).format.codecs); assertEquals(Format.NO_VALUE, variants.get(4).format.width); assertEquals(Format.NO_VALUE, variants.get(4).format.height); assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url); } + public void testMasterPlaylistWithBandwdithAverage() throws IOException { + HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, + AVG_BANDWIDTH_MASTER_PLAYLIST); + + List variants = masterPlaylist.variants; + + assertEquals(1280000, variants.get(0).format.bitrate); + assertEquals(1270000, variants.get(1).format.bitrate); + } + public void testPlaylistWithInvalidHeader() throws IOException { try { parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index cd6c0283b5..69759f83d0 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -75,7 +75,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Fri, 26 May 2017 08:31:41 -0700 Subject: [PATCH 072/220] Move Period and Window to the top of timeline to match Exoplayer style ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157223090 --- .../google/android/exoplayer2/Timeline.java | 450 +++++++++--------- 1 file changed, 225 insertions(+), 225 deletions(-) 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 1a33985c68..ec2f57685f 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 @@ -91,6 +91,231 @@ package com.google.android.exoplayer2; */ public abstract class Timeline { + /** + * Holds information about a window in a {@link Timeline}. A window defines a region of media + * currently available for playback along with additional information such as whether seeking is + * supported within the window. See {@link Timeline} for more details. The figure below shows some + * of the information defined by a window, as well as how this information relates to + * corresponding {@link Period}s in the timeline. + *

      + * Information defined by a timeline window + *

      + */ + public static final class Window { + + /** + * An identifier for the window. Not necessarily unique. + */ + public Object id; + + /** + * 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. + */ + public long presentationStartTimeMs; + + /** + * The window's start time in milliseconds since the epoch, or {@link C#TIME_UNSET} if unknown + * or not applicable. For informational purposes only. + */ + public long windowStartTimeMs; + + /** + * Whether it's possible to seek within this window. + */ + public boolean isSeekable; + + /** + * Whether this window may change when the timeline is updated. + */ + public boolean isDynamic; + + /** + * The index of the first period that belongs to this window. + */ + public int firstPeriodIndex; + + /** + * The index of the last period that belongs to this window. + */ + public int lastPeriodIndex; + + /** + * The default position relative to the start of the window at which to begin playback, in + * microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a + * non-zero default position projection, and if the specified projection cannot be performed + * whilst remaining within the bounds of the window. + */ + public long defaultPositionUs; + + /** + * The duration of this window in microseconds, or {@link C#TIME_UNSET} if unknown. + */ + public long durationUs; + + /** + * The position of the start of this window relative to the start of the first period belonging + * to it, in microseconds. + */ + public long positionInFirstPeriodUs; + + /** + * Sets the data held by this window. + */ + public Window set(Object id, long presentationStartTimeMs, long windowStartTimeMs, + boolean isSeekable, boolean isDynamic, long defaultPositionUs, long durationUs, + int firstPeriodIndex, int lastPeriodIndex, long positionInFirstPeriodUs) { + this.id = id; + this.presentationStartTimeMs = presentationStartTimeMs; + this.windowStartTimeMs = windowStartTimeMs; + this.isSeekable = isSeekable; + this.isDynamic = isDynamic; + this.defaultPositionUs = defaultPositionUs; + this.durationUs = durationUs; + this.firstPeriodIndex = firstPeriodIndex; + this.lastPeriodIndex = lastPeriodIndex; + this.positionInFirstPeriodUs = positionInFirstPeriodUs; + return this; + } + + /** + * Returns the default position relative to the start of the window at which to begin playback, + * in milliseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a + * non-zero default position projection, and if the specified projection cannot be performed + * whilst remaining within the bounds of the window. + */ + public long getDefaultPositionMs() { + return C.usToMs(defaultPositionUs); + } + + /** + * Returns the default position relative to the start of the window at which to begin playback, + * in microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a + * non-zero default position projection, and if the specified projection cannot be performed + * whilst remaining within the bounds of the window. + */ + public long getDefaultPositionUs() { + return defaultPositionUs; + } + + /** + * Returns the duration of the window in milliseconds, or {@link C#TIME_UNSET} if unknown. + */ + public long getDurationMs() { + return C.usToMs(durationUs); + } + + /** + * Returns the duration of this window in microseconds, or {@link C#TIME_UNSET} if unknown. + */ + public long getDurationUs() { + return durationUs; + } + + /** + * Returns the position of the start of this window relative to the start of the first period + * belonging to it, in milliseconds. + */ + public long getPositionInFirstPeriodMs() { + return C.usToMs(positionInFirstPeriodUs); + } + + /** + * Returns the position of the start of this window relative to the start of the first period + * belonging to it, in microseconds. + */ + public long getPositionInFirstPeriodUs() { + return positionInFirstPeriodUs; + } + + } + + /** + * Holds information about a period in a {@link Timeline}. A period defines a single logical piece + * of media, for example a a media file. See {@link Timeline} for more details. The figure below + * shows some of the information defined by a period, as well as how this information relates to a + * corresponding {@link Window} in the timeline. + *

      + * Information defined by a period + *

      + */ + public static final class Period { + + /** + * An identifier for the period. Not necessarily unique. + */ + public Object id; + + /** + * A unique identifier for the period. + */ + public Object uid; + + /** + * The index of the window to which this period belongs. + */ + public int windowIndex; + + /** + * The duration of this period in microseconds, or {@link C#TIME_UNSET} if unknown. + */ + public long durationUs; + + /** + * Whether this period contains an ad. + */ + public boolean isAd; + + private long positionInWindowUs; + + /** + * Sets the data held by this period. + */ + public Period set(Object id, Object uid, int windowIndex, long durationUs, + long positionInWindowUs, boolean isAd) { + this.id = id; + this.uid = uid; + this.windowIndex = windowIndex; + this.durationUs = durationUs; + this.positionInWindowUs = positionInWindowUs; + this.isAd = isAd; + return this; + } + + /** + * Returns the duration of the period in milliseconds, or {@link C#TIME_UNSET} if unknown. + */ + public long getDurationMs() { + return C.usToMs(durationUs); + } + + /** + * Returns the duration of this period in microseconds, or {@link C#TIME_UNSET} if unknown. + */ + public long getDurationUs() { + return durationUs; + } + + /** + * Returns the position of the start of this period relative to the start of the window to which + * it belongs, in milliseconds. May be negative if the start of the period is not within the + * window. + */ + public long getPositionInWindowMs() { + return C.usToMs(positionInWindowUs); + } + + /** + * Returns the position of the start of this period relative to the start of the window to which + * it belongs, in microseconds. May be negative if the start of the period is not within the + * window. + */ + public long getPositionInWindowUs() { + return positionInWindowUs; + } + + } + /** * An empty timeline. */ @@ -317,229 +542,4 @@ public abstract class Timeline { */ public abstract int getIndexOfPeriod(Object uid); - /** - * Holds information about a window in a {@link Timeline}. A window defines a region of media - * currently available for playback along with additional information such as whether seeking is - * supported within the window. See {@link Timeline} for more details. The figure below shows some - * of the information defined by a window, as well as how this information relates to - * corresponding {@link Period}s in the timeline. - *

      - * Information defined by a timeline window - *

      - */ - public static final class Window { - - /** - * An identifier for the window. Not necessarily unique. - */ - public Object id; - - /** - * 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. - */ - public long presentationStartTimeMs; - - /** - * The window's start time in milliseconds since the epoch, or {@link C#TIME_UNSET} if unknown - * or not applicable. For informational purposes only. - */ - public long windowStartTimeMs; - - /** - * Whether it's possible to seek within this window. - */ - public boolean isSeekable; - - /** - * Whether this window may change when the timeline is updated. - */ - public boolean isDynamic; - - /** - * The index of the first period that belongs to this window. - */ - public int firstPeriodIndex; - - /** - * The index of the last period that belongs to this window. - */ - public int lastPeriodIndex; - - /** - * The default position relative to the start of the window at which to begin playback, in - * microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a - * non-zero default position projection, and if the specified projection cannot be performed - * whilst remaining within the bounds of the window. - */ - public long defaultPositionUs; - - /** - * The duration of this window in microseconds, or {@link C#TIME_UNSET} if unknown. - */ - public long durationUs; - - /** - * The position of the start of this window relative to the start of the first period belonging - * to it, in microseconds. - */ - public long positionInFirstPeriodUs; - - /** - * Sets the data held by this window. - */ - public Window set(Object id, long presentationStartTimeMs, long windowStartTimeMs, - boolean isSeekable, boolean isDynamic, long defaultPositionUs, long durationUs, - int firstPeriodIndex, int lastPeriodIndex, long positionInFirstPeriodUs) { - this.id = id; - this.presentationStartTimeMs = presentationStartTimeMs; - this.windowStartTimeMs = windowStartTimeMs; - this.isSeekable = isSeekable; - this.isDynamic = isDynamic; - this.defaultPositionUs = defaultPositionUs; - this.durationUs = durationUs; - this.firstPeriodIndex = firstPeriodIndex; - this.lastPeriodIndex = lastPeriodIndex; - this.positionInFirstPeriodUs = positionInFirstPeriodUs; - return this; - } - - /** - * Returns the default position relative to the start of the window at which to begin playback, - * in milliseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a - * non-zero default position projection, and if the specified projection cannot be performed - * whilst remaining within the bounds of the window. - */ - public long getDefaultPositionMs() { - return C.usToMs(defaultPositionUs); - } - - /** - * Returns the default position relative to the start of the window at which to begin playback, - * in microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a - * non-zero default position projection, and if the specified projection cannot be performed - * whilst remaining within the bounds of the window. - */ - public long getDefaultPositionUs() { - return defaultPositionUs; - } - - /** - * Returns the duration of the window in milliseconds, or {@link C#TIME_UNSET} if unknown. - */ - public long getDurationMs() { - return C.usToMs(durationUs); - } - - /** - * Returns the duration of this window in microseconds, or {@link C#TIME_UNSET} if unknown. - */ - public long getDurationUs() { - return durationUs; - } - - /** - * Returns the position of the start of this window relative to the start of the first period - * belonging to it, in milliseconds. - */ - public long getPositionInFirstPeriodMs() { - return C.usToMs(positionInFirstPeriodUs); - } - - /** - * Returns the position of the start of this window relative to the start of the first period - * belonging to it, in microseconds. - */ - public long getPositionInFirstPeriodUs() { - return positionInFirstPeriodUs; - } - - } - - /** - * Holds information about a period in a {@link Timeline}. A period defines a single logical piece - * of media, for example a a media file. See {@link Timeline} for more details. The figure below - * shows some of the information defined by a period, as well as how this information relates to a - * corresponding {@link Window} in the timeline. - *

      - * Information defined by a period - *

      - */ - public static final class Period { - - /** - * An identifier for the period. Not necessarily unique. - */ - public Object id; - - /** - * A unique identifier for the period. - */ - public Object uid; - - /** - * The index of the window to which this period belongs. - */ - public int windowIndex; - - /** - * The duration of this period in microseconds, or {@link C#TIME_UNSET} if unknown. - */ - public long durationUs; - - /** - * Whether this period contains an ad. - */ - public boolean isAd; - - private long positionInWindowUs; - - /** - * Sets the data held by this period. - */ - public Period set(Object id, Object uid, int windowIndex, long durationUs, - long positionInWindowUs, boolean isAd) { - this.id = id; - this.uid = uid; - this.windowIndex = windowIndex; - this.durationUs = durationUs; - this.positionInWindowUs = positionInWindowUs; - this.isAd = isAd; - return this; - } - - /** - * Returns the duration of the period in milliseconds, or {@link C#TIME_UNSET} if unknown. - */ - public long getDurationMs() { - return C.usToMs(durationUs); - } - - /** - * Returns the duration of this period in microseconds, or {@link C#TIME_UNSET} if unknown. - */ - public long getDurationUs() { - return durationUs; - } - - /** - * Returns the position of the start of this period relative to the start of the window to which - * it belongs, in milliseconds. May be negative if the start of the period is not within the - * window. - */ - public long getPositionInWindowMs() { - return C.usToMs(positionInWindowUs); - } - - /** - * Returns the position of the start of this period relative to the start of the window to which - * it belongs, in microseconds. May be negative if the start of the period is not within the - * window. - */ - public long getPositionInWindowUs() { - return positionInWindowUs; - } - - } - } From 27fc82f0adf05c0246e5d1d6548cfb8ea41df11d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 26 May 2017 09:14:14 -0700 Subject: [PATCH 073/220] Make repeatMode private ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157226768 --- .../main/java/com/google/android/exoplayer2/ExoPlayerImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 11b1a46cf8..f0e0ffc9c1 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 @@ -51,7 +51,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private boolean tracksSelected; private boolean playWhenReady; - @RepeatMode int repeatMode; + private @RepeatMode int repeatMode; private int playbackState; private int pendingSeekAcks; private int pendingPrepareAcks; From 122b2a1a3187a36a9d8e13f6add87cb3e2e1c2ba Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 May 2017 10:36:29 -0700 Subject: [PATCH 074/220] Use single TrackGroup for switchable adaptation sets Issue: #2431 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157236031 --- .../source/dash/DashChunkSource.java | 4 +- .../source/dash/DashMediaPeriod.java | 211 ++++++++++++------ .../source/dash/DefaultDashChunkSource.java | 54 +++-- 3 files changed, 177 insertions(+), 92 deletions(-) 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 72f728092c..4e25c0e333 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 @@ -28,8 +28,8 @@ public interface DashChunkSource extends ChunkSource { interface Factory { DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, - DashManifest manifest, int periodIndex, int adaptationSetIndex, - TrackSelection trackSelection, long elapsedRealtimeOffsetMs, + DashManifest manifest, int periodIndex, int[] adaptationSetIndices, + TrackSelection trackSelection, int type, long elapsedRealtimeOffsetMs, boolean enableEventMessageTrack, boolean enableCea608Track); } 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 56fc532f9b..0d6b7e28ef 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.dash; import android.util.Pair; +import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; @@ -37,6 +38,7 @@ import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -55,7 +57,7 @@ import java.util.List; private final LoaderErrorThrower manifestLoaderErrorThrower; private final Allocator allocator; private final TrackGroupArray trackGroups; - private final EmbeddedTrackInfo[] embeddedTrackInfos; + private final TrackGroupInfo[] trackGroupInfos; private Callback callback; private ChunkSampleStream[] sampleStreams; @@ -80,9 +82,9 @@ import java.util.List; sampleStreams = newSampleStreamArray(0); sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; - Pair result = buildTrackGroups(adaptationSets); + Pair result = buildTrackGroups(adaptationSets); trackGroups = result.first; - embeddedTrackInfos = result.second; + trackGroupInfos = result.second; } public void updateManifest(DashManifest manifest, int periodIndex) { @@ -122,7 +124,6 @@ import java.util.List; @Override public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - int adaptationSetCount = adaptationSets.size(); HashMap> primarySampleStreams = new HashMap<>(); // First pass for primary tracks. for (int i = 0; i < selections.length; i++) { @@ -133,14 +134,15 @@ import java.util.List; stream.release(); streams[i] = null; } else { - int adaptationSetIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - primarySampleStreams.put(adaptationSetIndex, stream); + int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); + primarySampleStreams.put(trackGroupIndex, stream); } } if (streams[i] == null && selections[i] != null) { int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - if (trackGroupIndex < adaptationSetCount) { - ChunkSampleStream stream = buildSampleStream(trackGroupIndex, + TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; + if (trackGroupInfo.isPrimary) { + ChunkSampleStream stream = buildSampleStream(trackGroupInfo, selections[i], positionUs); primarySampleStreams.put(trackGroupIndex, stream); streams[i] = stream; @@ -160,11 +162,10 @@ import java.util.List; // may have been replaced, selected or deselected. if (selections[i] != null) { int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - if (trackGroupIndex >= adaptationSetCount) { - int embeddedTrackIndex = trackGroupIndex - adaptationSetCount; - EmbeddedTrackInfo embeddedTrackInfo = embeddedTrackInfos[embeddedTrackIndex]; - int adaptationSetIndex = embeddedTrackInfo.adaptationSetIndex; - ChunkSampleStream primaryStream = primarySampleStreams.get(adaptationSetIndex); + TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; + if (!trackGroupInfo.isPrimary) { + ChunkSampleStream primaryStream = primarySampleStreams.get( + trackGroupInfo.primaryTrackGroupIndex); SampleStream stream = streams[i]; boolean mayRetainStream = primaryStream == null ? stream instanceof EmptySampleStream : (stream instanceof EmbeddedSampleStream @@ -172,7 +173,7 @@ import java.util.List; if (!mayRetainStream) { releaseIfEmbeddedSampleStream(stream); streams[i] = primaryStream == null ? new EmptySampleStream() - : primaryStream.selectEmbeddedTrack(positionUs, embeddedTrackInfo.trackType); + : primaryStream.selectEmbeddedTrack(positionUs, trackGroupInfo.trackType); streamResetFlags[i] = true; } } @@ -235,49 +236,114 @@ import java.util.List; // Internal methods. - private static Pair buildTrackGroups( + private static Pair buildTrackGroups( List adaptationSets) { - int adaptationSetCount = adaptationSets.size(); - int embeddedTrackCount = getEmbeddedTrackCount(adaptationSets); - TrackGroup[] trackGroupArray = new TrackGroup[adaptationSetCount + embeddedTrackCount]; - EmbeddedTrackInfo[] embeddedTrackInfos = new EmbeddedTrackInfo[embeddedTrackCount]; + int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets); - int embeddedTrackIndex = 0; - for (int i = 0; i < adaptationSetCount; i++) { - AdaptationSet adaptationSet = adaptationSets.get(i); - List representations = adaptationSet.representations; + int primaryGroupCount = groupedAdaptationSetIndices.length; + boolean[] primaryGroupHasEventMessageTrackFlags = new boolean[primaryGroupCount]; + boolean[] primaryGroupHasCea608TrackFlags = new boolean[primaryGroupCount]; + int totalGroupCount = primaryGroupCount; + for (int i = 0; i < primaryGroupCount; i++) { + if (hasEventMessageTrack(adaptationSets, groupedAdaptationSetIndices[i])) { + primaryGroupHasEventMessageTrackFlags[i] = true; + totalGroupCount++; + } + if (hasCea608Track(adaptationSets, groupedAdaptationSetIndices[i])) { + primaryGroupHasCea608TrackFlags[i] = true; + totalGroupCount++; + } + } + + TrackGroup[] trackGroups = new TrackGroup[totalGroupCount]; + TrackGroupInfo[] trackGroupInfos = new TrackGroupInfo[totalGroupCount]; + + int trackGroupCount = 0; + for (int i = 0; i < primaryGroupCount; i++) { + int[] adaptationSetIndices = groupedAdaptationSetIndices[i]; + List representations = new ArrayList<>(); + for (int adaptationSetIndex : adaptationSetIndices) { + representations.addAll(adaptationSets.get(adaptationSetIndex).representations); + } Format[] formats = new Format[representations.size()]; for (int j = 0; j < formats.length; j++) { formats[j] = representations.get(j).format; } - trackGroupArray[i] = new TrackGroup(formats); - if (hasEventMessageTrack(adaptationSet)) { - Format format = Format.createSampleFormat(adaptationSet.id + ":emsg", + + AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]); + int primaryTrackGroupIndex = trackGroupCount; + boolean hasEventMessageTrack = primaryGroupHasEventMessageTrackFlags[i]; + boolean hasCea608Track = primaryGroupHasEventMessageTrackFlags[i]; + + trackGroups[trackGroupCount] = new TrackGroup(formats); + trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(firstAdaptationSet.type, + adaptationSetIndices, primaryTrackGroupIndex, true, hasEventMessageTrack, hasCea608Track); + if (hasEventMessageTrack) { + Format format = Format.createSampleFormat(firstAdaptationSet.id + ":emsg", MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null); - trackGroupArray[adaptationSetCount + embeddedTrackIndex] = new TrackGroup(format); - embeddedTrackInfos[embeddedTrackIndex++] = new EmbeddedTrackInfo(i, C.TRACK_TYPE_METADATA); + trackGroups[trackGroupCount] = new TrackGroup(format); + trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(C.TRACK_TYPE_METADATA, + adaptationSetIndices, primaryTrackGroupIndex, false, false, false); } - if (hasCea608Track(adaptationSet)) { - Format format = Format.createTextSampleFormat(adaptationSet.id + ":cea608", + if (hasCea608Track) { + Format format = Format.createTextSampleFormat(firstAdaptationSet.id + ":cea608", MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null); - trackGroupArray[adaptationSetCount + embeddedTrackIndex] = new TrackGroup(format); - embeddedTrackInfos[embeddedTrackIndex++] = new EmbeddedTrackInfo(i, C.TRACK_TYPE_TEXT); + trackGroups[trackGroupCount] = new TrackGroup(format); + trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(C.TRACK_TYPE_TEXT, + adaptationSetIndices, primaryTrackGroupIndex, false, false, false); } } - return Pair.create(new TrackGroupArray(trackGroupArray), embeddedTrackInfos); + return Pair.create(new TrackGroupArray(trackGroups), trackGroupInfos); } - private ChunkSampleStream buildSampleStream(int adaptationSetIndex, + private static int[][] getGroupedAdaptationSetIndices(List adaptationSets) { + int adaptationSetCount = adaptationSets.size(); + SparseIntArray idToIndexMap = new SparseIntArray(adaptationSetCount); + for (int i = 0; i < adaptationSetCount; i++) { + idToIndexMap.put(adaptationSets.get(i).id, i); + } + + int[][] groupedAdaptationSetIndices = new int[adaptationSetCount][]; + boolean[] adaptationSetUsedFlags = new boolean[adaptationSetCount]; + + int groupCount = 0; + for (int i = 0; i < adaptationSetCount; i++) { + if (adaptationSetUsedFlags[i]) { + // This adaptation set has already been included in a group. + continue; + } + adaptationSetUsedFlags[i] = true; + Descriptor adaptationSetSwitchingProperty = findAdaptationSetSwitchingProperty( + adaptationSets.get(i).supplementalProperties); + if (adaptationSetSwitchingProperty == null) { + groupedAdaptationSetIndices[groupCount++] = new int[] {i}; + } else { + String[] extraAdaptationSetIds = adaptationSetSwitchingProperty.value.split(","); + int[] adaptationSetIndices = new int[1 + extraAdaptationSetIds.length]; + adaptationSetIndices[0] = i; + for (int j = 0; j < extraAdaptationSetIds.length; j++) { + int extraIndex = idToIndexMap.get(Integer.parseInt(extraAdaptationSetIds[j])); + adaptationSetUsedFlags[extraIndex] = true; + adaptationSetIndices[1 + j] = extraIndex; + } + groupedAdaptationSetIndices[groupCount++] = adaptationSetIndices; + } + } + + return groupCount < adaptationSetCount + ? Arrays.copyOf(groupedAdaptationSetIndices, groupCount) : groupedAdaptationSetIndices; + } + + private ChunkSampleStream buildSampleStream(TrackGroupInfo trackGroupInfo, TrackSelection selection, long positionUs) { - AdaptationSet adaptationSet = adaptationSets.get(adaptationSetIndex); int embeddedTrackCount = 0; int[] embeddedTrackTypes = new int[2]; - boolean enableEventMessageTrack = hasEventMessageTrack(adaptationSet); + boolean enableEventMessageTrack = trackGroupInfo.hasEmbeddedEventMessageTrack; if (enableEventMessageTrack) { embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_METADATA; } - boolean enableCea608Track = hasCea608Track(adaptationSet); + boolean enableCea608Track = trackGroupInfo.hasEmbeddedCea608Track; if (enableCea608Track) { embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_TEXT; } @@ -285,45 +351,48 @@ import java.util.List; embeddedTrackTypes = Arrays.copyOf(embeddedTrackTypes, embeddedTrackCount); } DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource( - manifestLoaderErrorThrower, manifest, periodIndex, adaptationSetIndex, selection, - elapsedRealtimeOffset, enableEventMessageTrack, enableCea608Track); - ChunkSampleStream stream = new ChunkSampleStream<>(adaptationSet.type, + manifestLoaderErrorThrower, manifest, periodIndex, trackGroupInfo.adaptationSetIndices, + selection, trackGroupInfo.trackType, elapsedRealtimeOffset, enableEventMessageTrack, + enableCea608Track); + ChunkSampleStream stream = new ChunkSampleStream<>(trackGroupInfo.trackType, embeddedTrackTypes, chunkSource, this, allocator, positionUs, minLoadableRetryCount, eventDispatcher); return stream; } - private static int getEmbeddedTrackCount(List adaptationSets) { - int embeddedTrackCount = 0; - for (int i = 0; i < adaptationSets.size(); i++) { - AdaptationSet adaptationSet = adaptationSets.get(i); - if (hasEventMessageTrack(adaptationSet)) { - embeddedTrackCount++; - } - if (hasCea608Track(adaptationSet)) { - embeddedTrackCount++; + private static Descriptor findAdaptationSetSwitchingProperty(List descriptors) { + for (int i = 0; i < descriptors.size(); i++) { + Descriptor descriptor = descriptors.get(i); + if ("urn:mpeg:dash:adaptation-set-switching:2016".equals(descriptor.schemeIdUri)) { + return descriptor; } } - return embeddedTrackCount; + return null; } - private static boolean hasEventMessageTrack(AdaptationSet adaptationSet) { - List representations = adaptationSet.representations; - for (int i = 0; i < representations.size(); i++) { - Representation representation = representations.get(i); - if (!representation.inbandEventStreams.isEmpty()) { - return true; + private static boolean hasEventMessageTrack(List adaptationSets, + int[] adaptationSetIndices) { + for (int i : adaptationSetIndices) { + List representations = adaptationSets.get(i).representations; + for (int j = 0; j < representations.size(); j++) { + Representation representation = representations.get(j); + if (!representation.inbandEventStreams.isEmpty()) { + return true; + } } } return false; } - private static boolean hasCea608Track(AdaptationSet adaptationSet) { - List descriptors = adaptationSet.accessibilityDescriptors; - for (int i = 0; i < descriptors.size(); i++) { - Descriptor descriptor = descriptors.get(i); - if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)) { - return true; + private static boolean hasCea608Track(List adaptationSets, + int[] adaptationSetIndices) { + for (int i : adaptationSetIndices) { + List descriptors = adaptationSets.get(i).accessibilityDescriptors; + for (int j = 0; j < descriptors.size(); j++) { + Descriptor descriptor = descriptors.get(j); + if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)) { + return true; + } } } return false; @@ -340,14 +409,24 @@ import java.util.List; } } - private static final class EmbeddedTrackInfo { + private static final class TrackGroupInfo { - public final int adaptationSetIndex; + public final int[] adaptationSetIndices; public final int trackType; + public final boolean isPrimary; - public EmbeddedTrackInfo(int adaptationSetIndex, int trackType) { - this.adaptationSetIndex = adaptationSetIndex; + public final int primaryTrackGroupIndex; + public final boolean hasEmbeddedEventMessageTrack; + public final boolean hasEmbeddedCea608Track; + + public TrackGroupInfo(int trackType, int[] adaptationSetIndices, int primaryTrackGroupIndex, + boolean isPrimary, boolean hasEmbeddedEventMessageTrack, boolean hasEmbeddedCea608Track) { this.trackType = trackType; + this.adaptationSetIndices = adaptationSetIndices; + this.primaryTrackGroupIndex = primaryTrackGroupIndex; + this.isPrimary = isPrimary; + this.hasEmbeddedEventMessageTrack = hasEmbeddedEventMessageTrack; + this.hasEmbeddedCea608Track = hasEmbeddedCea608Track; } } 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 e679ef635c..297052f65a 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 @@ -46,6 +46,7 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower; 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.List; /** @@ -69,20 +70,21 @@ public class DefaultDashChunkSource implements DashChunkSource { @Override public DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, - DashManifest manifest, int periodIndex, int adaptationSetIndex, - TrackSelection trackSelection, long elapsedRealtimeOffsetMs, + DashManifest manifest, int periodIndex, int[] adaptationSetIndices, + TrackSelection trackSelection, int trackType, long elapsedRealtimeOffsetMs, boolean enableEventMessageTrack, boolean enableCea608Track) { DataSource dataSource = dataSourceFactory.createDataSource(); return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex, - adaptationSetIndex, trackSelection, dataSource, elapsedRealtimeOffsetMs, + adaptationSetIndices, trackSelection, trackType, dataSource, elapsedRealtimeOffsetMs, maxSegmentsPerLoad, enableEventMessageTrack, enableCea608Track); } } private final LoaderErrorThrower manifestLoaderErrorThrower; - private final int adaptationSetIndex; + private final int[] adaptationSetIndices; private final TrackSelection trackSelection; + private final int trackType; private final RepresentationHolder[] representationHolders; private final DataSource dataSource; private final long elapsedRealtimeOffsetMs; @@ -98,8 +100,9 @@ public class DefaultDashChunkSource implements DashChunkSource { * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. * @param manifest The initial manifest. * @param periodIndex The index of the period in the manifest. - * @param adaptationSetIndex The index of the adaptation set in the period. + * @param adaptationSetIndices The indices of the adaptation sets in the period. * @param trackSelection The track selection. + * @param trackType The type of the tracks in the selection. * @param dataSource A {@link DataSource} suitable for loading the media data. * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between * server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified @@ -112,26 +115,27 @@ public class DefaultDashChunkSource implements DashChunkSource { * @param enableCea608Track Whether the chunks generated by the source may output a CEA-608 track. */ public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, - DashManifest manifest, int periodIndex, int adaptationSetIndex, TrackSelection trackSelection, - DataSource dataSource, long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad, - boolean enableEventMessageTrack, boolean enableCea608Track) { + DashManifest manifest, int periodIndex, int[] adaptationSetIndices, + TrackSelection trackSelection, int trackType, DataSource dataSource, + long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad, boolean enableEventMessageTrack, + boolean enableCea608Track) { this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifest = manifest; - this.adaptationSetIndex = adaptationSetIndex; + this.adaptationSetIndices = adaptationSetIndices; this.trackSelection = trackSelection; + this.trackType = trackType; this.dataSource = dataSource; this.periodIndex = periodIndex; this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs; this.maxSegmentsPerLoad = maxSegmentsPerLoad; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - AdaptationSet adaptationSet = getAdaptationSet(); - List representations = adaptationSet.representations; + List representations = getRepresentations(); representationHolders = new RepresentationHolder[trackSelection.length()]; for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); representationHolders[i] = new RepresentationHolder(periodDurationUs, representation, - enableEventMessageTrack, enableCea608Track, adaptationSet.type); + enableEventMessageTrack, enableCea608Track); } } @@ -141,7 +145,7 @@ public class DefaultDashChunkSource implements DashChunkSource { manifest = newManifest; periodIndex = newPeriodIndex; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - List representations = getAdaptationSet().representations; + List representations = getRepresentations(); for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); representationHolders[i].updateRepresentation(periodDurationUs, representation); @@ -248,9 +252,9 @@ public class DefaultDashChunkSource implements DashChunkSource { } int maxSegmentCount = Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1); - out.chunk = newMediaChunk(representationHolder, dataSource, trackSelection.getSelectedFormat(), - trackSelection.getSelectionReason(), trackSelection.getSelectionData(), segmentNum, - maxSegmentCount); + out.chunk = newMediaChunk(representationHolder, dataSource, trackType, + trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(), + trackSelection.getSelectionData(), segmentNum, maxSegmentCount); } @Override @@ -298,8 +302,13 @@ public class DefaultDashChunkSource implements DashChunkSource { // Private methods. - private AdaptationSet getAdaptationSet() { - return manifest.getPeriod(periodIndex).adaptationSets.get(adaptationSetIndex); + private ArrayList getRepresentations() { + List manifestAdapationSets = manifest.getPeriod(periodIndex).adaptationSets; + ArrayList representations = new ArrayList<>(); + for (int adaptationSetIndex : adaptationSetIndices) { + representations.addAll(manifestAdapationSets.get(adaptationSetIndex).representations); + } + return representations; } private long getNowUnixTimeUs() { @@ -332,7 +341,7 @@ public class DefaultDashChunkSource implements DashChunkSource { } private static Chunk newMediaChunk(RepresentationHolder representationHolder, - DataSource dataSource, Format trackFormat, int trackSelectionReason, + DataSource dataSource, int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, int firstSegmentNum, int maxSegmentCount) { Representation representation = representationHolder.representation; long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum); @@ -343,8 +352,7 @@ public class DefaultDashChunkSource implements DashChunkSource { DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl), segmentUri.start, segmentUri.length, representation.getCacheKey()); return new SingleSampleMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, - trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, - representationHolder.trackType, trackFormat); + trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, trackType, trackFormat); } else { int segmentCount = 1; for (int i = 1; i < maxSegmentCount; i++) { @@ -371,7 +379,6 @@ public class DefaultDashChunkSource implements DashChunkSource { protected static final class RepresentationHolder { - public final int trackType; public final ChunkExtractorWrapper extractorWrapper; public Representation representation; @@ -381,10 +388,9 @@ public class DefaultDashChunkSource implements DashChunkSource { private int segmentNumShift; public RepresentationHolder(long periodDurationUs, Representation representation, - boolean enableEventMessageTrack, boolean enableCea608Track, int trackType) { + boolean enableEventMessageTrack, boolean enableCea608Track) { this.periodDurationUs = periodDurationUs; this.representation = representation; - this.trackType = trackType; String containerMimeType = representation.format.containerMimeType; if (mimeTypeIsRawText(containerMimeType)) { extractorWrapper = null; From 12ef97fc34376838e3eb09f16fc371d952577b64 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 26 May 2017 11:33:09 -0700 Subject: [PATCH 075/220] Fix and complete MediaPlaylist javadocs ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157243533 --- .../source/hls/playlist/HlsMediaPlaylist.java | 60 +++++++++++++++---- 1 file changed, 50 insertions(+), 10 deletions(-) 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 102fe6ee86..db4f041be2 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 @@ -33,24 +33,64 @@ public final class HlsMediaPlaylist extends HlsPlaylist { */ public static final class Segment implements Comparable { + /** + * The url of the segment. + */ public final String url; + /** + * The duration of the segment in microseconds, as defined by #EXTINF. + */ public final long durationUs; + /** + * The number of #EXT-X-DISCONTINUITY tags in the playlist before the segment. + */ public final int relativeDiscontinuitySequence; + /** + * The start time of the segment in microseconds, relative to the start of the playlist. + */ public final long relativeStartTimeUs; + /** + * Whether the segment is encrypted, as defined by #EXT-X-KEY. + */ public final boolean isEncrypted; + /** + * The encryption key uri as defined by #EXT-X-KEY, or null if the segment is not encrypted. + */ public final String encryptionKeyUri; + /** + * The encryption initialization vector as defined by #EXT-X-KEY, or null if the segment is not + * encrypted. + */ public final String encryptionIV; + /** + * The segment's byte range offset, as defined by #EXT-X-BYTERANGE. + */ public final long byterangeOffset; + /** + * The segment's byte range length, as defined by #EXT-X-BYTERANGE, or {@link C#LENGTH_UNSET} if + * no byte range is specified. + */ public final long byterangeLength; public Segment(String uri, long byterangeOffset, long byterangeLength) { this(uri, 0, -1, C.TIME_UNSET, false, null, null, byterangeOffset, byterangeLength); } - public Segment(String uri, long durationUs, int relativeDiscontinuitySequence, + /** + * @param url See {@link #url}. + * @param durationUs See {@link #durationUs}. + * @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}. + * @param relativeStartTimeUs See {@link #relativeStartTimeUs}. + * @param isEncrypted See {@link #isEncrypted}. + * @param encryptionKeyUri See {@link #encryptionKeyUri}. + * @param encryptionIV See {@link #encryptionIV}. + * @param byterangeOffset See {@link #byterangeOffset}. + * @param byterangeLength See {@link #byterangeLength}. + */ + public Segment(String url, long durationUs, int relativeDiscontinuitySequence, long relativeStartTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV, long byterangeOffset, long byterangeLength) { - this.url = uri; + this.url = url; this.durationUs = durationUs; this.relativeDiscontinuitySequence = relativeDiscontinuitySequence; this.relativeStartTimeUs = relativeStartTimeUs; @@ -84,7 +124,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { */ @PlaylistType public final int playlistType; /** - * The start offset as defined by #EXT-X-START in microseconds. + * The start offset in microseconds, as defined by #EXT-X-START. */ public final long startOffsetUs; /** @@ -96,19 +136,21 @@ public final class HlsMediaPlaylist extends HlsPlaylist { */ public final boolean hasDiscontinuitySequence; /** - * The discontinuity sequence number. + * The discontinuity sequence number of the first media segment in the playlist, as defined by + * #EXT-X-DISCONTINUITY-SEQUENCE. */ public final int discontinuitySequence; /** - * The media sequence number as defined by #EXT-X-MEDIA-SEQUENCE. + * The media sequence number of the first media segment in the playlist, as defined by + * #EXT-X-MEDIA-SEQUENCE. */ public final int mediaSequence; /** - * The compatibility version as defined by #EXT-X-VERSION. + * The compatibility version, as defined by #EXT-X-VERSION. */ public final int version; /** - * The target duration as defined by #EXT-X-TARGETDURATION in microseconds. + * The target duration in microseconds, as defined by #EXT-X-TARGETDURATION. */ public final long targetDurationUs; /** @@ -124,7 +166,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { */ public final boolean hasProgramDateTime; /** - * The initialization segment as defined by #EXT-X-MAP. + * The initialization segment, as defined by #EXT-X-MAP. */ public final Segment initializationSegment; /** @@ -203,8 +245,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist { /** * Returns the result of adding the duration of the playlist to its start time. - * - * @return The result of adding the duration of the playlist to its start time. */ public long getEndTimeUs() { return startTimeUs + durationUs; From 7d4eaa74f76a49e7547a177801005133a9810ce3 Mon Sep 17 00:00:00 2001 From: anjalibh Date: Wed, 7 Jun 2017 15:02:35 +0100 Subject: [PATCH 076/220] Expose the HDR ColorInfo in the VpxOutputBuffer so the renderer can use it. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157267699 --- .../ext/vp9/LibvpxVideoRenderer.java | 3 +- .../exoplayer2/ext/vp9/VpxDecoder.java | 12 +++---- .../exoplayer2/ext/vp9/VpxInputBuffer.java | 32 +++++++++++++++++++ .../exoplayer2/ext/vp9/VpxOutputBuffer.java | 3 ++ 4 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.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 4b629c8d2a..467c19b06a 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 @@ -71,7 +71,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private DecoderCounters decoderCounters; private Format format; private VpxDecoder decoder; - private DecoderInputBuffer inputBuffer; + private VpxInputBuffer inputBuffer; private VpxOutputBuffer outputBuffer; private VpxOutputBuffer nextOutputBuffer; private DrmSession drmSession; @@ -394,6 +394,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { return false; } inputBuffer.flip(); + inputBuffer.colorInfo = formatHolder.format.colorInfo; decoder.queueInputBuffer(inputBuffer); decoderCounters.inputBufferCount++; inputBuffer = 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 73ec7c2f96..4bec5bdf4c 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 @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.vp9; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.CryptoInfo; -import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.drm.DecryptionException; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -27,7 +26,7 @@ import java.nio.ByteBuffer; * Vpx decoder. */ /* package */ final class VpxDecoder extends - SimpleDecoder { + SimpleDecoder { public static final int OUTPUT_MODE_NONE = -1; public static final int OUTPUT_MODE_YUV = 0; @@ -54,7 +53,7 @@ import java.nio.ByteBuffer; */ public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, ExoMediaCrypto exoMediaCrypto) throws VpxDecoderException { - super(new DecoderInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]); + super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]); if (!VpxLibrary.isAvailable()) { throw new VpxDecoderException("Failed to load decoder native libraries."); } @@ -85,8 +84,8 @@ import java.nio.ByteBuffer; } @Override - protected DecoderInputBuffer createInputBuffer() { - return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); + protected VpxInputBuffer createInputBuffer() { + return new VpxInputBuffer(); } @Override @@ -100,7 +99,7 @@ import java.nio.ByteBuffer; } @Override - protected VpxDecoderException decode(DecoderInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, + protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) { ByteBuffer inputData = inputBuffer.data; int inputSize = inputData.limit(); @@ -128,6 +127,7 @@ import java.nio.ByteBuffer; } else if (getFrameResult == -1) { return new VpxDecoderException("Buffer initialization failed."); } + outputBuffer.colorInfo = inputBuffer.colorInfo; return null; } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java new file mode 100644 index 0000000000..fcae9dc6bc --- /dev/null +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java @@ -0,0 +1,32 @@ +/* + * 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.ext.vp9; + +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 { + + public ColorInfo colorInfo; + + public VpxInputBuffer() { + super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); + } + +} 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 db3cf49b0c..2618bf7c62 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.vp9; import com.google.android.exoplayer2.decoder.OutputBuffer; +import com.google.android.exoplayer2.video.ColorInfo; import java.nio.ByteBuffer; /** @@ -37,6 +38,8 @@ import java.nio.ByteBuffer; public ByteBuffer data; public int width; public int height; + public ColorInfo colorInfo; + /** * YUV planes for YUV mode. */ From bcd4bf0fd5e729168b2ed125dc28eae0ed3dc0b7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 31 May 2017 01:46:49 -0700 Subject: [PATCH 077/220] Fix DefaultTimeBar invalidation Issue: #2871 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157562792 --- .../java/com/google/android/exoplayer2/ui/DefaultTimeBar.java | 4 ++++ 1 file changed, 4 insertions(+) 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 fd05fdd5d0..d9754420bf 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 @@ -220,11 +220,13 @@ public class DefaultTimeBar extends View implements TimeBar { public void setPosition(long position) { this.position = position; setContentDescription(getProgressText()); + update(); } @Override public void setBufferedPosition(long bufferedPosition) { this.bufferedPosition = bufferedPosition; + update(); } @Override @@ -235,6 +237,7 @@ public class DefaultTimeBar extends View implements TimeBar { } else { updateScrubberState(); } + update(); } @Override @@ -242,6 +245,7 @@ public class DefaultTimeBar extends View implements TimeBar { Assertions.checkArgument(adBreakCount == 0 || adBreakTimesMs != null); this.adBreakCount = adBreakCount; this.adBreakTimesMs = adBreakTimesMs; + update(); } @Override From b1fd99baaba91b3a8610666e60f4bc196cd556ff Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 31 May 2017 03:16:47 -0700 Subject: [PATCH 078/220] Constraint seeks within bounds for ConstantBitrateSeeker We do this everywhere for index based seeking already. Issue: #2876 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157568788 --- .../exoplayer2/extractor/mp3/ConstantBitrateSeeker.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java index c5de8d8284..df7748a910 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.mp3; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Util; /** * MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate. @@ -41,8 +42,11 @@ import com.google.android.exoplayer2.C; @Override public long getPosition(long timeUs) { - return durationUs == C.TIME_UNSET ? 0 - : firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); + if (durationUs == C.TIME_UNSET) { + return 0; + } + timeUs = Util.constrainValue(timeUs, 0, durationUs); + return firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); } @Override From 0f27efae4465e0e48fd18850f2efe42de63046e1 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 31 May 2017 03:59:24 -0700 Subject: [PATCH 079/220] Ignore invalid EXT-X-PLAYLIST-TYPE values Issue:#2889 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157571216 --- .../exoplayer2/source/hls/playlist/HlsPlaylistParser.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 69759f83d0..ba9bd50194 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -323,8 +323,6 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Wed, 31 May 2017 07:23:13 -0700 Subject: [PATCH 080/220] Move adaptation disabling workaround into MediaCodecUtil This is necessary to make sure that the correct thing happens where MediaCodecInfo.adaptive is queried directly (for example, MediaCodecVideoRenderer uses the field to determine how to size input buffers). Also disable adaptive on Nexus 10. Issue: #2806 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157583473 --- .../exoplayer2/mediacodec/MediaCodecInfo.java | 23 +++- .../mediacodec/MediaCodecRenderer.java | 16 +-- .../exoplayer2/mediacodec/MediaCodecUtil.java | 105 ++++++++++-------- 3 files changed, 79 insertions(+), 65 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 6914b2f52c..3c788a60a4 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 @@ -71,7 +71,7 @@ public final class MediaCodecInfo { * @return The created instance. */ public static MediaCodecInfo newPassthroughInstance(String name) { - return new MediaCodecInfo(name, null, null); + return new MediaCodecInfo(name, null, null, false); } /** @@ -84,18 +84,29 @@ public final class MediaCodecInfo { */ public static MediaCodecInfo newInstance(String name, String mimeType, CodecCapabilities capabilities) { - return new MediaCodecInfo(name, mimeType, capabilities); + return new MediaCodecInfo(name, mimeType, capabilities, false); } /** - * @param name The name of the decoder. - * @param capabilities The capabilities of the decoder. + * Creates an instance. + * + * @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. + * @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}. + * @return The created instance. */ - private MediaCodecInfo(String name, String mimeType, CodecCapabilities capabilities) { + public static MediaCodecInfo newInstance(String name, String mimeType, + CodecCapabilities capabilities, boolean forceDisableAdaptive) { + return new MediaCodecInfo(name, mimeType, capabilities, forceDisableAdaptive); + } + + private MediaCodecInfo(String name, String mimeType, CodecCapabilities capabilities, + boolean forceDisableAdaptive) { this.name = Assertions.checkNotNull(name); this.mimeType = mimeType; this.capabilities = capabilities; - adaptive = capabilities != null && isAdaptive(capabilities); + adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities); tunneling = capabilities != null && isTunneling(capabilities); } 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 d58dbc4065..ebc0a0cda3 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 @@ -339,7 +339,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } String codecName = decoderInfo.name; - codecIsAdaptive = decoderInfo.adaptive && !codecNeedsDisableAdaptationWorkaround(codecName); + codecIsAdaptive = decoderInfo.adaptive; codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName); @@ -1188,18 +1188,4 @@ public abstract class MediaCodecRenderer extends BaseRenderer { && "OMX.MTK.AUDIO.DECODER.MP3".equals(name); } - /** - * Returns whether the decoder is known to fail when adapting, despite advertising itself as an - * adaptive decoder. - *

      - * If true is returned then we explicitly disable adaptation for the decoder. - * - * @param name The decoder name. - * @return True if the decoder is known to fail when adapting. - */ - private static boolean codecNeedsDisableAdaptationWorkaround(String name) { - return Util.SDK_INT <= 19 && Util.MODEL.equals("ODROID-XU3") - && ("OMX.Exynos.AVC.Decoder".equals(name) || "OMX.Exynos.AVC.Decoder.secure".equals(name)); - } - } 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 2bb3603df9..5369dffeb6 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 @@ -160,6 +160,55 @@ public final class MediaCodecUtil { return decoderInfos; } + /** + * Returns the maximum frame size supported by the default H264 decoder. + * + * @return The maximum frame size for an H264 stream that can be decoded on the device. + */ + public static int maxH264DecodableFrameSize() throws DecoderQueryException { + if (maxH264DecodableFrameSize == -1) { + int result = 0; + MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false); + if (decoderInfo != null) { + for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) { + result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result); + } + // We assume support for at least 480p (SDK_INT >= 21) or 360p (SDK_INT < 21), which are + // the levels mandated by the Android CDD. + result = Math.max(result, Util.SDK_INT >= 21 ? (720 * 480) : (480 * 360)); + } + maxH264DecodableFrameSize = result; + } + return maxH264DecodableFrameSize; + } + + /** + * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the given + * codec description string (as defined by RFC 6381). + * + * @param codec A codec description string, as defined by RFC 6381. + * @return A pair (profile constant, level constant) if {@code codec} is well-formed and + * recognized, or null otherwise + */ + public static Pair getCodecProfileAndLevel(String codec) { + if (codec == null) { + return null; + } + String[] parts = codec.split("\\."); + switch (parts[0]) { + case CODEC_ID_HEV1: + case CODEC_ID_HVC1: + return getHevcProfileAndLevel(codec, parts); + case CODEC_ID_AVC1: + case CODEC_ID_AVC2: + return getAvcProfileAndLevel(codec, parts); + default: + return null; + } + } + + // Internal methods. + private static List getDecoderInfosInternal( CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException { try { @@ -177,12 +226,14 @@ public final class MediaCodecUtil { try { CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(supportedType); boolean secure = mediaCodecList.isSecurePlaybackSupported(mimeType, capabilities); + boolean forceDisableAdaptive = codecNeedsDisableAdaptationWorkaround(codecName); if ((secureDecodersExplicit && key.secure == secure) || (!secureDecodersExplicit && !key.secure)) { - decoderInfos.add(MediaCodecInfo.newInstance(codecName, mimeType, capabilities)); + decoderInfos.add(MediaCodecInfo.newInstance(codecName, mimeType, capabilities, + forceDisableAdaptive)); } else if (!secureDecodersExplicit && secure) { decoderInfos.add(MediaCodecInfo.newInstance(codecName + ".secure", mimeType, - capabilities)); + capabilities, forceDisableAdaptive)); // It only makes sense to have one synthesized secure decoder, return immediately. return decoderInfos; } @@ -289,50 +340,16 @@ public final class MediaCodecUtil { } /** - * Returns the maximum frame size supported by the default H264 decoder. + * Returns whether the decoder is known to fail when adapting, despite advertising itself as an + * adaptive decoder. * - * @return The maximum frame size for an H264 stream that can be decoded on the device. + * @param name The decoder name. + * @return True if the decoder is known to fail when adapting. */ - public static int maxH264DecodableFrameSize() throws DecoderQueryException { - if (maxH264DecodableFrameSize == -1) { - int result = 0; - MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false); - if (decoderInfo != null) { - for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) { - result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result); - } - // We assume support for at least 480p (SDK_INT >= 21) or 360p (SDK_INT < 21), which are - // the levels mandated by the Android CDD. - result = Math.max(result, Util.SDK_INT >= 21 ? (720 * 480) : (480 * 360)); - } - maxH264DecodableFrameSize = result; - } - return maxH264DecodableFrameSize; - } - - /** - * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the given - * codec description string (as defined by RFC 6381). - * - * @param codec A codec description string, as defined by RFC 6381. - * @return A pair (profile constant, level constant) if {@code codec} is well-formed and - * recognized, or null otherwise - */ - public static Pair getCodecProfileAndLevel(String codec) { - if (codec == null) { - return null; - } - String[] parts = codec.split("\\."); - switch (parts[0]) { - case CODEC_ID_HEV1: - case CODEC_ID_HVC1: - return getHevcProfileAndLevel(codec, parts); - case CODEC_ID_AVC1: - case CODEC_ID_AVC2: - return getAvcProfileAndLevel(codec, parts); - default: - return null; - } + private static boolean codecNeedsDisableAdaptationWorkaround(String name) { + return Util.SDK_INT <= 22 + && (Util.MODEL.equals("ODROID-XU3") || Util.MODEL.equals("Nexus 10")) + && ("OMX.Exynos.AVC.Decoder".equals(name) || "OMX.Exynos.AVC.Decoder.secure".equals(name)); } private static Pair getHevcProfileAndLevel(String codec, String[] parts) { From 32b5a802918ab01018fb39f6ee964ed7275d7b0a Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Jun 2017 07:39:33 -0700 Subject: [PATCH 081/220] Deprecate LoopingMediaSource for indefinite looping ExoPlayer.setRepeatMode should be preferred. Deprecate the constructor and update the relevant documentation. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157829207 --- .../android/exoplayer2/source/LoopingMediaSource.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 0e1e7d9033..a97f7ecd95 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 @@ -23,19 +23,21 @@ import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; /** - * Loops a {@link MediaSource}. + * Loops a {@link MediaSource} a specified number of times. + *

      + * Note: To loop a {@link MediaSource} indefinitely, it is usually better to use + * {@link ExoPlayer#setRepeatMode(int)}. */ public final class LoopingMediaSource implements MediaSource { - private static final String TAG = "LoopingMediaSource"; - private final MediaSource childSource; private final int loopCount; private int childPeriodCount; /** - * Loops the provided source indefinitely. + * Loops the provided source indefinitely. Note that it is usually better to use + * {@link ExoPlayer#setRepeatMode(int)}. * * @param childSource The {@link MediaSource} to loop. */ From ba9114c9c7691b38d91cdd00eae73ab3743860d5 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Jun 2017 08:10:58 -0700 Subject: [PATCH 082/220] Automatically use DummySurface when possible Issue: #677 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157831796 --- .../android/exoplayer2/demo/EventLogger.java | 2 +- .../ext/vp9/LibvpxVideoRenderer.java | 14 +-- .../exoplayer2/mediacodec/MediaCodecInfo.java | 49 +++++++---- .../mediacodec/MediaCodecRenderer.java | 87 ++++++++++--------- .../exoplayer2/mediacodec/MediaCodecUtil.java | 4 +- .../exoplayer2/video/DummySurface.java | 4 +- .../video/MediaCodecVideoRenderer.java | 84 +++++++++++++++--- 7 files changed, 167 insertions(+), 77 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 686718f9e0..87c85f6800 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -281,7 +281,7 @@ import java.util.Locale; @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - // Do nothing. + Log.d(TAG, "videoSizeChanged [" + width + ", " + height + "]"); } @Override 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 467c19b06a..e661aae892 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 @@ -255,7 +255,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) { // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. - if (outputBuffer.timeUs <= positionUs) { + if (isBufferLate(outputBuffer.timeUs - positionUs)) { skipBuffer(); return true; } @@ -280,7 +280,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer { return false; } - /** * Returns whether the current frame should be dropped. * @@ -293,10 +292,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer { */ protected boolean shouldDropOutputBuffer(long outputBufferTimeUs, long nextOutputBufferTimeUs, long positionUs, long joiningDeadlineMs) { - // Drop the frame if we're joining and are more than 30ms late, or if we have the next frame - // and that's also late. Else we'll render what we have. - return (joiningDeadlineMs != C.TIME_UNSET && outputBufferTimeUs < positionUs - 30000) - || (nextOutputBufferTimeUs != C.TIME_UNSET && nextOutputBufferTimeUs < positionUs); + return isBufferLate(outputBufferTimeUs - positionUs) + && (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET); } private void renderBuffer() { @@ -655,4 +652,9 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } + private static boolean isBufferLate(long earlyUs) { + // Class a buffer as late if it should have been presented more than 30ms ago. + return earlyUs < -30000; + } + } 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 3c788a60a4..6ff5082cbd 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 @@ -61,6 +61,14 @@ public final class MediaCodecInfo { */ public final boolean tunneling; + /** + * Whether the decoder is secure. + * + * @see CodecCapabilities#isFeatureRequired(String) + * @see CodecCapabilities#FEATURE_SecurePlayback + */ + public final boolean secure; + private final String mimeType; private final CodecCapabilities capabilities; @@ -71,7 +79,7 @@ public final class MediaCodecInfo { * @return The created instance. */ public static MediaCodecInfo newPassthroughInstance(String name) { - return new MediaCodecInfo(name, null, null, false); + return new MediaCodecInfo(name, null, null, false, false); } /** @@ -84,7 +92,7 @@ public final class MediaCodecInfo { */ public static MediaCodecInfo newInstance(String name, String mimeType, CodecCapabilities capabilities) { - return new MediaCodecInfo(name, mimeType, capabilities, false); + return new MediaCodecInfo(name, mimeType, capabilities, false, false); } /** @@ -94,20 +102,22 @@ public final class MediaCodecInfo { * @param mimeType A mime type supported by the {@link MediaCodec}. * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type. * @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}. + * @param forceSecure Whether {@link #secure} should be forced to {@code true}. * @return The created instance. */ public static MediaCodecInfo newInstance(String name, String mimeType, - CodecCapabilities capabilities, boolean forceDisableAdaptive) { - return new MediaCodecInfo(name, mimeType, capabilities, forceDisableAdaptive); + CodecCapabilities capabilities, boolean forceDisableAdaptive, boolean forceSecure) { + return new MediaCodecInfo(name, mimeType, capabilities, forceDisableAdaptive, forceSecure); } private MediaCodecInfo(String name, String mimeType, CodecCapabilities capabilities, - boolean forceDisableAdaptive) { + boolean forceDisableAdaptive, boolean forceSecure) { this.name = Assertions.checkNotNull(name); this.mimeType = mimeType; this.capabilities = capabilities; adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities); tunneling = capabilities != null && isTunneling(capabilities); + secure = forceSecure || (capabilities != null && isSecure(capabilities)); } /** @@ -176,12 +186,12 @@ public final class MediaCodecInfo { logNoSupport("sizeAndRate.vCaps"); return false; } - if (!areSizeAndRateSupported(videoCapabilities, width, height, frameRate)) { + if (!areSizeAndRateSupportedV21(videoCapabilities, width, height, frameRate)) { // Capabilities are known to be inaccurately reported for vertical resolutions on some devices // (b/31387661). If the video is vertical and the capabilities indicate support if the width // and height are swapped, we assume that the vertical resolution is also supported. if (width >= height - || !areSizeAndRateSupported(videoCapabilities, height, width, frameRate)) { + || !areSizeAndRateSupportedV21(videoCapabilities, height, width, frameRate)) { logNoSupport("sizeAndRate.support, " + width + "x" + height + "x" + frameRate); return false; } @@ -290,14 +300,6 @@ public final class MediaCodecInfo { return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback); } - @TargetApi(21) - private static boolean areSizeAndRateSupported(VideoCapabilities capabilities, int width, - int height, double frameRate) { - return frameRate == Format.NO_VALUE || frameRate <= 0 - ? capabilities.isSizeSupported(width, height) - : capabilities.areSizeAndRateSupported(width, height, frameRate); - } - private static boolean isTunneling(CodecCapabilities capabilities) { return Util.SDK_INT >= 21 && isTunnelingV21(capabilities); } @@ -307,4 +309,21 @@ public final class MediaCodecInfo { return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback); } + private static boolean isSecure(CodecCapabilities capabilities) { + return Util.SDK_INT >= 21 && isSecureV21(capabilities); + } + + @TargetApi(21) + private static boolean isSecureV21(CodecCapabilities capabilities) { + return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback); + } + + @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); + } + } 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 ebc0a0cda3..6c0010407b 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 @@ -175,10 +175,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private final MediaCodec.BufferInfo outputBufferInfo; private Format format; - private MediaCodec codec; private DrmSession drmSession; private DrmSession pendingDrmSession; - private boolean codecIsAdaptive; + private MediaCodec codec; + private MediaCodecInfo codecInfo; private boolean codecNeedsDiscardToSpsWorkaround; private boolean codecNeedsFlushWorkaround; private boolean codecNeedsAdaptationWorkaround; @@ -291,7 +291,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @SuppressWarnings("deprecation") protected final void maybeInitCodec() throws ExoPlaybackException { - if (!shouldInitCodec()) { + if (codec != null || format == null) { + // We have a codec already, or we don't have a format with which to instantiate one. return; } @@ -313,18 +314,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } - MediaCodecInfo decoderInfo = null; + MediaCodecInfo codecInfo = null; try { - decoderInfo = getDecoderInfo(mediaCodecSelector, format, drmSessionRequiresSecureDecoder); - if (decoderInfo == null && drmSessionRequiresSecureDecoder) { + codecInfo = getDecoderInfo(mediaCodecSelector, format, drmSessionRequiresSecureDecoder); + if (codecInfo == null && drmSessionRequiresSecureDecoder) { // The drm session indicates that a secure decoder is required, but the device does not have // one. Assuming that supportsFormat indicated support for the media being played, we know // that it does not require a secure output path. Most CDM implementations allow playback to // proceed with a non-secure decoder in this case, so we try our luck. - decoderInfo = getDecoderInfo(mediaCodecSelector, format, false); - if (decoderInfo != null) { + codecInfo = getDecoderInfo(mediaCodecSelector, format, false); + if (codecInfo != null) { Log.w(TAG, "Drm session requires secure decoder for " + mimeType + ", but " - + "no secure decoder available. Trying to proceed with " + decoderInfo.name + "."); + + "no secure decoder available. Trying to proceed with " + codecInfo.name + "."); } } } catch (DecoderQueryException e) { @@ -332,14 +333,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer { drmSessionRequiresSecureDecoder, DecoderInitializationException.DECODER_QUERY_ERROR)); } - if (decoderInfo == null) { + if (codecInfo == null) { throwDecoderInitError(new DecoderInitializationException(format, null, drmSessionRequiresSecureDecoder, DecoderInitializationException.NO_SUITABLE_DECODER_ERROR)); } - String codecName = decoderInfo.name; - codecIsAdaptive = decoderInfo.adaptive; + if (!shouldInitCodec(codecInfo)) { + return; + } + + this.codecInfo = codecInfo; + String codecName = codecInfo.name; codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName); @@ -353,7 +358,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codec = MediaCodec.createByCodecName(codecName); TraceUtil.endSection(); TraceUtil.beginSection("configureCodec"); - configureCodec(decoderInfo, codec, format, mediaCrypto); + configureCodec(codecInfo, codec, format, mediaCrypto); TraceUtil.endSection(); TraceUtil.beginSection("startCodec"); codec.start(); @@ -380,14 +385,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer { throw ExoPlaybackException.createForRenderer(e, getIndex()); } - protected boolean shouldInitCodec() { - return codec == null && format != null; + protected boolean shouldInitCodec(MediaCodecInfo codecInfo) { + return true; } protected final MediaCodec getCodec() { return codec; } + protected final MediaCodecInfo getCodecInfo() { + return codecInfo; + } + @Override protected void onEnabled(boolean joining) throws ExoPlaybackException { decoderCounters = new DecoderCounters(); @@ -426,31 +435,31 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } protected void releaseCodec() { + codecHotswapDeadlineMs = C.TIME_UNSET; + inputIndex = C.INDEX_UNSET; + outputIndex = C.INDEX_UNSET; + waitingForKeys = false; + shouldSkipOutputBuffer = false; + decodeOnlyPresentationTimestamps.clear(); + inputBuffers = null; + outputBuffers = null; + codecInfo = null; + codecReconfigured = false; + codecReceivedBuffers = false; + codecNeedsDiscardToSpsWorkaround = false; + codecNeedsFlushWorkaround = false; + codecNeedsAdaptationWorkaround = false; + codecNeedsEosPropagationWorkaround = false; + codecNeedsEosFlushWorkaround = false; + codecNeedsMonoChannelCountWorkaround = false; + codecNeedsAdaptationWorkaroundBuffer = false; + shouldSkipAdaptationWorkaroundOutputBuffer = false; + codecReceivedEos = false; + codecReconfigurationState = RECONFIGURATION_STATE_NONE; + codecReinitializationState = REINITIALIZATION_STATE_NONE; + buffer.data = null; if (codec != null) { - codecHotswapDeadlineMs = C.TIME_UNSET; - inputIndex = C.INDEX_UNSET; - outputIndex = C.INDEX_UNSET; - waitingForKeys = false; - shouldSkipOutputBuffer = false; - decodeOnlyPresentationTimestamps.clear(); - inputBuffers = null; - outputBuffers = null; - codecReconfigured = false; - codecReceivedBuffers = false; - codecIsAdaptive = false; - codecNeedsDiscardToSpsWorkaround = false; - codecNeedsFlushWorkaround = false; - codecNeedsAdaptationWorkaround = false; - codecNeedsEosPropagationWorkaround = false; - codecNeedsEosFlushWorkaround = false; - codecNeedsMonoChannelCountWorkaround = false; - codecNeedsAdaptationWorkaroundBuffer = false; - shouldSkipAdaptationWorkaroundOutputBuffer = false; - codecReceivedEos = false; - codecReconfigurationState = RECONFIGURATION_STATE_NONE; - codecReinitializationState = REINITIALIZATION_STATE_NONE; decoderCounters.decoderReleaseCount++; - buffer.data = null; try { codec.stop(); } finally { @@ -781,7 +790,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } if (pendingDrmSession == drmSession && codec != null - && canReconfigureCodec(codec, codecIsAdaptive, oldFormat, format)) { + && canReconfigureCodec(codec, codecInfo.adaptive, oldFormat, format)) { codecReconfigured = true; codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; codecNeedsAdaptationWorkaroundBuffer = codecNeedsAdaptationWorkaround 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 5369dffeb6..6d34da2c2e 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,10 +230,10 @@ public final class MediaCodecUtil { if ((secureDecodersExplicit && key.secure == secure) || (!secureDecodersExplicit && !key.secure)) { decoderInfos.add(MediaCodecInfo.newInstance(codecName, mimeType, capabilities, - forceDisableAdaptive)); + forceDisableAdaptive, false)); } else if (!secureDecodersExplicit && secure) { decoderInfos.add(MediaCodecInfo.newInstance(codecName + ".secure", mimeType, - capabilities, forceDisableAdaptive)); + capabilities, forceDisableAdaptive, true)); // It only makes sense to have one synthesized secure decoder, return immediately. return decoderInfos; } 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 5298c82f61..81b396cfc7 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 @@ -255,8 +255,8 @@ public final class DummySurface extends Surface { if (secure) { glAttributes = new int[] { EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_PROTECTED_CONTENT_EXT, - EGL_TRUE, EGL_NONE}; + EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, + EGL_NONE}; } else { glAttributes = new int[] { EGL_CONTEXT_CLIENT_VERSION, 2, 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 aabec0eaa7..cb248fd142 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 @@ -40,6 +40,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; @@ -77,6 +78,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private CodecMaxValues codecMaxValues; private Surface surface; + private Surface dummySurface; @C.VideoScalingMode private int scalingMode; private boolean renderedFirstFrame; @@ -263,7 +265,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override public boolean isReady() { - if ((renderedFirstFrame || super.shouldInitCodec()) && super.isReady()) { + if (super.isReady() && (renderedFirstFrame || (dummySurface != null && surface == dummySurface) + || getCodec() == null)) { // Ready. If we were joining then we've now joined, so clear the joining deadline. joiningDeadlineMs = C.TIME_UNSET; return true; @@ -306,6 +309,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { clearRenderedFirstFrame(); frameReleaseTimeHelper.disable(); tunnelingOnFrameRenderedListener = null; + tunneling = false; try { super.onDisabled(); } finally { @@ -330,6 +334,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } private void setSurface(Surface surface) throws ExoPlaybackException { + if (surface == null) { + // Use a dummy surface if possible. + if (dummySurface != null) { + surface = dummySurface; + } else { + MediaCodecInfo codecInfo = getCodecInfo(); + if (codecInfo != null && shouldUseDummySurface(codecInfo.secure)) { + dummySurface = DummySurface.newInstanceV17(codecInfo.secure); + surface = dummySurface; + } + } + } // We only need to update the codec if the surface has changed. if (this.surface != surface) { this.surface = surface; @@ -343,7 +359,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { maybeInitCodec(); } } - if (surface != null) { + if (surface != null && surface != dummySurface) { // If we know the video size, report it again immediately. maybeRenotifyVideoSizeChanged(); // We haven't rendered to the new surface yet. @@ -356,17 +372,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { clearReportedVideoSize(); clearRenderedFirstFrame(); } - } else if (surface != null) { - // The surface is unchanged and non-null. If we know the video size and/or have already - // rendered to the surface, report these again immediately. + } else if (surface != null && surface != dummySurface) { + // The surface is set and unchanged. If we know the video size and/or have already rendered to + // the surface, report these again immediately. maybeRenotifyVideoSizeChanged(); maybeRenotifyRenderedFirstFrame(); } } @Override - protected boolean shouldInitCodec() { - return super.shouldInitCodec() && surface != null && surface.isValid(); + protected boolean shouldInitCodec(MediaCodecInfo codecInfo) { + return surface != null || shouldUseDummySurface(codecInfo.secure); } @Override @@ -375,12 +391,34 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { codecMaxValues = getCodecMaxValues(codecInfo, format, streamFormats); MediaFormat mediaFormat = getMediaFormat(format, codecMaxValues, deviceNeedsAutoFrcWorkaround, tunnelingAudioSessionId); + if (surface == null) { + Assertions.checkState(shouldUseDummySurface(codecInfo.secure)); + if (dummySurface == null) { + dummySurface = DummySurface.newInstanceV17(codecInfo.secure); + } + surface = dummySurface; + } codec.configure(mediaFormat, surface, crypto, 0); if (Util.SDK_INT >= 23 && tunneling) { tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec); } } + @Override + protected void releaseCodec() { + try { + super.releaseCodec(); + } finally { + if (dummySurface != null) { + if (surface == dummySurface) { + surface = null; + } + dummySurface.release(); + dummySurface = null; + } + } + } + @Override protected void onCodecInitialized(String name, long initializedTimestampMs, long initializationDurationMs) { @@ -452,11 +490,22 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { pendingOutputStreamOffsetCount); } long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs; + if (shouldSkip) { skipOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } + long earlyUs = bufferPresentationTimeUs - positionUs; + if (surface == dummySurface) { + // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. + if (isBufferLate(earlyUs)) { + skipOutputBuffer(codec, bufferIndex, presentationTimeUs); + return true; + } + return false; + } + if (!renderedFirstFrame) { if (Util.SDK_INT >= 21) { renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, System.nanoTime()); @@ -470,9 +519,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return false; } - // Compute how many microseconds it is until the buffer's presentation time. + // Fine-grained adjustment of earlyUs based on the elapsed time since the start of the current + // iteration of the rendering loop. long elapsedSinceStartOfLoopUs = (SystemClock.elapsedRealtime() * 1000) - elapsedRealtimeUs; - long earlyUs = bufferPresentationTimeUs - positionUs - elapsedSinceStartOfLoopUs; + earlyUs -= elapsedSinceStartOfLoopUs; // Compute the buffer's desired release time in nanoseconds. long systemTimeNs = System.nanoTime(); @@ -484,7 +534,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { - // We're more than 30ms late rendering the frame. dropOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } @@ -526,8 +575,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * measured at the start of the current iteration of the rendering loop. */ protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) { - // Drop the frame if we're more than 30ms late rendering the frame. - return earlyUs < -30000; + return isBufferLate(earlyUs); } /** @@ -604,6 +652,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { maybeNotifyRenderedFirstFrame(); } + private boolean shouldUseDummySurface(boolean codecIsSecure) { + // TODO: Work out when we can safely uncomment the secure case below. This case is currently + // broken on Galaxy S8 [Internal: b/37197802]. + return Util.SDK_INT >= 23 && !tunneling + && (!codecIsSecure /* || DummySurface.SECURE_SUPPORTED */); + } + private void setJoiningDeadlineMs() { joiningDeadlineMs = allowedJoiningTimeMs > 0 ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; @@ -674,6 +729,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } + private static boolean isBufferLate(long earlyUs) { + // Class a buffer as late if it should have been presented more than 30ms ago. + return earlyUs < -30000; + } + @SuppressLint("InlinedApi") private static MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues, boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) { From e98bee6163a354754fe0b383c3e2160865d754be Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Jun 2017 08:26:13 -0700 Subject: [PATCH 083/220] Add surface attach/detach to *WithRendererDisabling tests This will cause the test to exercise the code path of instantiating a DummySurface, rendering to it for 10 seconds, then re-targeting the real surface again. For secure content tests the code path is only exercised if DummySurface.SECURE_SUPPORTED is true. The logic for checking this is within MediaCodecVideoRenderer itself, rather than being part of the test. Issue: #677 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157833026 --- .../video/MediaCodecVideoRenderer.java | 18 +++-- .../playbacktests/gts/DashStreamingTest.java | 4 ++ .../exoplayer2/playbacktests/util/Action.java | 68 ++++++++++++++++--- .../playbacktests/util/ActionSchedule.java | 46 ++++++++++--- .../playbacktests/util/ExoHostedTest.java | 6 +- 5 files changed, 118 insertions(+), 24 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index cb248fd142..6a51016dd3 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 @@ -653,10 +653,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } private boolean shouldUseDummySurface(boolean codecIsSecure) { - // TODO: Work out when we can safely uncomment the secure case below. This case is currently - // broken on Galaxy S8 [Internal: b/37197802]. - return Util.SDK_INT >= 23 && !tunneling - && (!codecIsSecure /* || DummySurface.SECURE_SUPPORTED */); + return Util.SDK_INT >= 23 && !tunneling && (!codecIsSecure + || (DummySurface.SECURE_SUPPORTED && !deviceNeedsSecureDummySurfaceWorkaround())); } private void setJoiningDeadlineMs() { @@ -923,6 +921,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { codec.setVideoScalingMode(scalingMode); } + /** + * Returns whether the device is known to fail outputting from a secure decoder to a secure + * surface texture. + *

      + * If true is returned then use of {@link DummySurface} is disabled for secure playbacks. + */ + private static boolean deviceNeedsSecureDummySurfaceWorkaround() { + // See [Internal: b/37197802]. + return Util.SDK_INT == 24 + && (Util.MODEL.startsWith("SM-G950") || Util.MODEL.startsWith("SM-G955")); + } + /** * Returns whether the device is known to enable frame-rate conversion logic that negatively * impacts ExoPlayer. diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java index e7441362cf..24f73e9d08 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java @@ -67,6 +67,10 @@ public final class DashStreamingTest extends ActivityInstrumentationTestCase2 Date: Fri, 2 Jun 2017 09:48:58 -0700 Subject: [PATCH 084/220] Assume CBR for MP3s with Info headers Issue: #2895 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157841519 --- .../extractor/mp3/Mp3Extractor.java | 118 +++++++++++------- 1 file changed, 73 insertions(+), 45 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 b0faad71c0..6e114137f1 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 @@ -87,10 +87,12 @@ public final class Mp3Extractor implements Extractor { /** * Mask that includes the audio header values that must match between frames. */ - private static final int HEADER_MASK = 0xFFFE0C00; - private static final int XING_HEADER = Util.getIntegerCodeForString("Xing"); - private static final int INFO_HEADER = Util.getIntegerCodeForString("Info"); - private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI"); + 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_UNSET = 0; @Flags private final int flags; private final long forcedFirstSampleTimestampUs; @@ -178,7 +180,11 @@ public final class Mp3Extractor implements Extractor { } } if (seeker == null) { - seeker = setupSeeker(input); + seeker = maybeReadSeekFrame(input); + if (seeker == null + || (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { + seeker = getConstantBitrateSeeker(input); + } extractorOutput.seekMap(seeker); trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, @@ -197,7 +203,7 @@ public final class Mp3Extractor implements Extractor { } scratch.setPosition(0); int sampleHeaderData = scratch.readInt(); - if ((sampleHeaderData & HEADER_MASK) != (synchronizedHeaderData & HEADER_MASK) + if (!headersMatch(sampleHeaderData, synchronizedHeaderData) || MpegAudioHeader.getFrameSize(sampleHeaderData) == C.LENGTH_UNSET) { // We have lost synchronization, so attempt to resynchronize starting at the next byte. extractorInput.skipFully(1); @@ -254,7 +260,7 @@ public final class Mp3Extractor implements Extractor { int headerData = scratch.readInt(); int frameSize; if ((candidateSynchronizedHeaderData != 0 - && (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK)) + && !headersMatch(headerData, candidateSynchronizedHeaderData)) || (frameSize = MpegAudioHeader.getFrameSize(headerData)) == C.LENGTH_UNSET) { // The header doesn't match the candidate header or is invalid. Try the next byte offset. if (searchedBytes++ == searchLimitBytes) { @@ -337,37 +343,27 @@ public final class Mp3Extractor implements Extractor { } /** - * Returns a {@link Seeker} to seek using metadata read from {@code input}, which should provide - * data from the start of the first frame in the stream. On returning, the input's position will - * be set to the start of the first frame of audio. + * Consumes the next frame from the {@code input} if it contains VBRI or Xing seeking metadata, + * returning a {@link Seeker} if the metadata was present and valid, or {@code null} otherwise. + * After this method returns, the input position is the start of the first frame of audio. * * @param input The {@link ExtractorInput} from which to read. + * @return A {@link Seeker} if seeking metadata was present and valid, or {@code null} otherwise. * @throws IOException Thrown if there was an error reading from the stream. Not expected if the * next two frames were already peeked during synchronization. * @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if * the next two frames were already peeked during synchronization. - * @return a {@link Seeker}. */ - private Seeker setupSeeker(ExtractorInput input) throws IOException, InterruptedException { - // Read the first frame which may contain a Xing or VBRI header with seeking metadata. + private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException, InterruptedException { ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize); input.peekFully(frame.data, 0, synchronizedHeader.frameSize); - - long position = input.getPosition(); - long length = input.getLength(); - int headerData = 0; - Seeker seeker = null; - - // Check if there is a Xing header. int xingBase = (synchronizedHeader.version & 1) != 0 ? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1 : (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5 - if (frame.limit() >= xingBase + 4) { - frame.setPosition(xingBase); - headerData = frame.readInt(); - } - if (headerData == XING_HEADER || headerData == INFO_HEADER) { - seeker = XingSeeker.create(synchronizedHeader, frame, position, length); + int seekHeader = getSeekFrameHeader(frame, xingBase); + Seeker seeker; + if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) { + seeker = XingSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength()); if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) { // If there is a Xing header, read gapless playback metadata at a fixed offset. input.resetPeekPosition(); @@ -377,28 +373,60 @@ public final class Mp3Extractor implements Extractor { gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24()); } input.skipFully(synchronizedHeader.frameSize); - } else if (frame.limit() >= 40) { - // Check if there is a VBRI header. - frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes. - headerData = frame.readInt(); - if (headerData == VBRI_HEADER) { - seeker = VbriSeeker.create(synchronizedHeader, frame, position, length); - input.skipFully(synchronizedHeader.frameSize); + if (seeker != null && !seeker.isSeekable() && seekHeader == SEEK_HEADER_INFO) { + // Fall back to constant bitrate seeking for Info headers missing a table of contents. + return getConstantBitrateSeeker(input); + } + } else if (seekHeader == SEEK_HEADER_VBRI) { + seeker = VbriSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength()); + input.skipFully(synchronizedHeader.frameSize); + } else { // seekerHeader == SEEK_HEADER_UNSET + // This frame doesn't contain seeking information, so reset the peek position. + seeker = null; + input.resetPeekPosition(); + } + return seeker; + } + + /** + * Peeks the next frame and returns a {@link ConstantBitrateSeeker} based on its bitrate. + */ + private Seeker getConstantBitrateSeeker(ExtractorInput input) + throws IOException, InterruptedException { + input.peekFully(scratch.data, 0, 4); + scratch.setPosition(0); + MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); + return new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, + input.getLength()); + } + + /** + * Returns whether the headers match in those bits masked by {@link #MPEG_AUDIO_HEADER_MASK}. + */ + private static boolean headersMatch(int headerA, long headerB) { + return (headerA & MPEG_AUDIO_HEADER_MASK) == (headerB & MPEG_AUDIO_HEADER_MASK); + } + + /** + * Returns {@link #SEEK_HEADER_XING}, {@link #SEEK_HEADER_INFO} or {@link #SEEK_HEADER_VBRI} if + * the provided {@code frame} may have seeking metadata, or {@link #SEEK_HEADER_UNSET} otherwise. + * If seeking metadata is present, {@code frame}'s position is advanced past the header. + */ + private static int getSeekFrameHeader(ParsableByteArray frame, int xingBase) { + if (frame.limit() >= xingBase + 4) { + frame.setPosition(xingBase); + int headerData = frame.readInt(); + if (headerData == SEEK_HEADER_XING || headerData == SEEK_HEADER_INFO) { + return headerData; } } - - if (seeker == null || (!seeker.isSeekable() - && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { - // Repopulate the synchronized header in case we had to skip an invalid seeking header, which - // would give an invalid CBR bitrate. - input.resetPeekPosition(); - input.peekFully(scratch.data, 0, 4); - scratch.setPosition(0); - MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); - seeker = new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, length); + if (frame.limit() >= 40) { + frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes. + if (frame.readInt() == SEEK_HEADER_VBRI) { + return SEEK_HEADER_VBRI; + } } - - return seeker; + return SEEK_HEADER_UNSET; } /** From 35cc0d65cde21395929e4750988d1502a0191ccb Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Jun 2017 10:40:39 -0700 Subject: [PATCH 085/220] Only update codecInfo when needed This avoids calling getDecoderInfo repeatedly in the case where shouldInitCodec return false (e.g. because we don't have a surface and cannot instantiate a dummy surface). Issue: #677 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157847702 --- .../mediacodec/MediaCodecRenderer.java | 46 +++++++++---------- .../mediacodec/MediaCodecSelector.java | 3 +- .../exoplayer2/mediacodec/MediaCodecUtil.java | 2 +- 3 files changed, 25 insertions(+), 26 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 6c0010407b..750ba1f6ec 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 @@ -314,36 +314,36 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } - MediaCodecInfo codecInfo = null; - try { - codecInfo = getDecoderInfo(mediaCodecSelector, format, drmSessionRequiresSecureDecoder); - if (codecInfo == null && drmSessionRequiresSecureDecoder) { - // The drm session indicates that a secure decoder is required, but the device does not have - // one. Assuming that supportsFormat indicated support for the media being played, we know - // that it does not require a secure output path. Most CDM implementations allow playback to - // proceed with a non-secure decoder in this case, so we try our luck. - codecInfo = getDecoderInfo(mediaCodecSelector, format, false); - if (codecInfo != null) { - Log.w(TAG, "Drm session requires secure decoder for " + mimeType + ", but " - + "no secure decoder available. Trying to proceed with " + codecInfo.name + "."); - } - } - } catch (DecoderQueryException e) { - throwDecoderInitError(new DecoderInitializationException(format, e, - drmSessionRequiresSecureDecoder, DecoderInitializationException.DECODER_QUERY_ERROR)); - } - if (codecInfo == null) { - throwDecoderInitError(new DecoderInitializationException(format, null, - drmSessionRequiresSecureDecoder, - DecoderInitializationException.NO_SUITABLE_DECODER_ERROR)); + try { + codecInfo = getDecoderInfo(mediaCodecSelector, format, drmSessionRequiresSecureDecoder); + if (codecInfo == null && drmSessionRequiresSecureDecoder) { + // The drm session indicates that a secure decoder is required, but the device does not + // have one. Assuming that supportsFormat indicated support for the media being played, we + // know that it does not require a secure output path. Most CDM implementations allow + // playback to proceed with a non-secure decoder in this case, so we try our luck. + codecInfo = getDecoderInfo(mediaCodecSelector, format, false); + if (codecInfo != null) { + Log.w(TAG, "Drm session requires secure decoder for " + mimeType + ", but " + + "no secure decoder available. Trying to proceed with " + codecInfo.name + "."); + } + } + } catch (DecoderQueryException e) { + throwDecoderInitError(new DecoderInitializationException(format, e, + drmSessionRequiresSecureDecoder, DecoderInitializationException.DECODER_QUERY_ERROR)); + } + + if (codecInfo == null) { + throwDecoderInitError(new DecoderInitializationException(format, null, + drmSessionRequiresSecureDecoder, + DecoderInitializationException.NO_SUITABLE_DECODER_ERROR)); + } } if (!shouldInitCodec(codecInfo)) { return; } - this.codecInfo = codecInfo; String codecName = codecInfo.name; codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); 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 bb946d76f9..1823c3a7ff 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 @@ -55,8 +55,7 @@ public interface MediaCodecSelector { /** * Selects a decoder to instantiate for audio passthrough. * - * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder - * exists. + * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists. * @throws DecoderQueryException Thrown if there was an error querying decoders. */ MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException; 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 6d34da2c2e..73ceff2754 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 @@ -99,7 +99,7 @@ public final class MediaCodecUtil { /** * Returns information about a decoder suitable for audio passthrough. - ** + * * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder * exists. */ From 8dca0b941886b4eab9246c0a6a36c5f0dc5e44b5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Sun, 4 Jun 2017 07:02:13 -0700 Subject: [PATCH 086/220] Add a null check in DummySurface static initializer ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157958694 --- .../java/com/google/android/exoplayer2/video/DummySurface.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 81b396cfc7..e998eceaaf 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 @@ -76,7 +76,7 @@ public final class DummySurface extends Surface { if (Util.SDK_INT >= 17) { EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); String extensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); - SECURE_SUPPORTED = extensions.contains("EGL_EXT_protected_content"); + SECURE_SUPPORTED = extensions != null && extensions.contains("EGL_EXT_protected_content"); } else { SECURE_SUPPORTED = false; } From 10c2d3156b3aa8b4322bbd5d523a07f9d3f8e8ee Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 5 Jun 2017 05:28:01 -0700 Subject: [PATCH 087/220] Pick the lowest quality video when capabilities are exceeded Issue:#2901 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158006727 --- .../exoplayer2/trackselection/DefaultTrackSelector.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 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 b37088e588..2a426c9c52 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 @@ -639,7 +639,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { continue; } int trackScore = isWithinConstraints ? 2 : 1; - if (isSupported(trackFormatSupport[trackIndex], false)) { + boolean isWithinCapabilities = isSupported(trackFormatSupport[trackIndex], false); + if (isWithinCapabilities) { trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; } boolean selectTrack = trackScore > selectedTrackScore; @@ -655,7 +656,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } else { comparisonResult = compareFormatValues(format.bitrate, selectedBitrate); } - selectTrack = isWithinConstraints ? comparisonResult > 0 : comparisonResult < 0; + selectTrack = isWithinCapabilities && isWithinConstraints + ? comparisonResult > 0 : comparisonResult < 0; } if (selectTrack) { selectedGroup = trackGroup; From edbc2046e2c6291e0e229638c78a507ea491c996 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 5 Jun 2017 16:03:39 -0700 Subject: [PATCH 088/220] Clean-up manifest merge attributes. 1. Remove tools:replace in manifest files. This attribute is only needed to establish priority when two manifests are merged and have the same attribute with different values. As this is not happening here, the attributes can be removed. 2. Some BUILD files also define a deprecated manifest merge strategy different from the android default merge strategy. For consistency these are set to "android'. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158075128 --- extensions/cronet/src/androidTest/AndroidManifest.xml | 3 +-- extensions/flac/src/androidTest/AndroidManifest.xml | 3 +-- extensions/opus/src/androidTest/AndroidManifest.xml | 3 +-- extensions/vp9/src/androidTest/AndroidManifest.xml | 3 +-- library/core/src/androidTest/AndroidManifest.xml | 3 +-- library/dash/src/androidTest/AndroidManifest.xml | 3 +-- library/hls/src/androidTest/AndroidManifest.xml | 3 +-- library/smoothstreaming/src/androidTest/AndroidManifest.xml | 3 +-- playbacktests/src/androidTest/AndroidManifest.xml | 3 +-- 9 files changed, 9 insertions(+), 18 deletions(-) diff --git a/extensions/cronet/src/androidTest/AndroidManifest.xml b/extensions/cronet/src/androidTest/AndroidManifest.xml index 2f45a1a2e5..1f371a1864 100644 --- a/extensions/cronet/src/androidTest/AndroidManifest.xml +++ b/extensions/cronet/src/androidTest/AndroidManifest.xml @@ -28,7 +28,6 @@ + android:targetPackage="com.google.android.exoplayer.ext.cronet"/> diff --git a/extensions/flac/src/androidTest/AndroidManifest.xml b/extensions/flac/src/androidTest/AndroidManifest.xml index 0a62db3bb5..73032ab50c 100644 --- a/extensions/flac/src/androidTest/AndroidManifest.xml +++ b/extensions/flac/src/androidTest/AndroidManifest.xml @@ -28,7 +28,6 @@ + android:name="android.test.InstrumentationTestRunner"/> diff --git a/extensions/opus/src/androidTest/AndroidManifest.xml b/extensions/opus/src/androidTest/AndroidManifest.xml index c819529692..e77590dc65 100644 --- a/extensions/opus/src/androidTest/AndroidManifest.xml +++ b/extensions/opus/src/androidTest/AndroidManifest.xml @@ -28,7 +28,6 @@ + android:name="android.test.InstrumentationTestRunner"/> diff --git a/extensions/vp9/src/androidTest/AndroidManifest.xml b/extensions/vp9/src/androidTest/AndroidManifest.xml index d9fa8af2c3..b8b28fc346 100644 --- a/extensions/vp9/src/androidTest/AndroidManifest.xml +++ b/extensions/vp9/src/androidTest/AndroidManifest.xml @@ -28,7 +28,6 @@ + android:name="android.test.InstrumentationTestRunner"/> diff --git a/library/core/src/androidTest/AndroidManifest.xml b/library/core/src/androidTest/AndroidManifest.xml index 9eab386b51..2634152c98 100644 --- a/library/core/src/androidTest/AndroidManifest.xml +++ b/library/core/src/androidTest/AndroidManifest.xml @@ -28,7 +28,6 @@ + android:name="android.test.InstrumentationTestRunner"/> diff --git a/library/dash/src/androidTest/AndroidManifest.xml b/library/dash/src/androidTest/AndroidManifest.xml index ac2511d3bd..a9b143253f 100644 --- a/library/dash/src/androidTest/AndroidManifest.xml +++ b/library/dash/src/androidTest/AndroidManifest.xml @@ -28,7 +28,6 @@ + android:name="android.test.InstrumentationTestRunner"/> diff --git a/library/hls/src/androidTest/AndroidManifest.xml b/library/hls/src/androidTest/AndroidManifest.xml index ac0857fc3f..dcf6c2f940 100644 --- a/library/hls/src/androidTest/AndroidManifest.xml +++ b/library/hls/src/androidTest/AndroidManifest.xml @@ -28,7 +28,6 @@ + android:name="android.test.InstrumentationTestRunner"/> diff --git a/library/smoothstreaming/src/androidTest/AndroidManifest.xml b/library/smoothstreaming/src/androidTest/AndroidManifest.xml index 9f62e26867..ab314ce806 100644 --- a/library/smoothstreaming/src/androidTest/AndroidManifest.xml +++ b/library/smoothstreaming/src/androidTest/AndroidManifest.xml @@ -28,7 +28,6 @@ + android:name="android.test.InstrumentationTestRunner"/> diff --git a/playbacktests/src/androidTest/AndroidManifest.xml b/playbacktests/src/androidTest/AndroidManifest.xml index 2f7bbe6d7c..64b927655f 100644 --- a/playbacktests/src/androidTest/AndroidManifest.xml +++ b/playbacktests/src/androidTest/AndroidManifest.xml @@ -36,7 +36,6 @@ + android:name="android.test.InstrumentationTestRunner"/> From 5c2c3c5c63aecfb8fe4b31db10aa7c48933cdfbb Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 6 Jun 2017 05:31:42 -0700 Subject: [PATCH 089/220] Create a base class for DASH downloading related tests ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158129802 --- .../google/android/exoplayer2/upstream/cache/SimpleCache.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 bbff7dc4a2..2da6ba759b 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 @@ -110,7 +110,8 @@ public final class SimpleCache implements Cache { @Override public synchronized NavigableSet getCachedSpans(String key) { CachedContent cachedContent = index.get(key); - return cachedContent == null ? null : new TreeSet(cachedContent.getSpans()); + return cachedContent == null || cachedContent.isEmpty() ? null + : new TreeSet(cachedContent.getSpans()); } @Override From c1bfab3c23a6c6eafc5842ba13f59cff1df8b6ee Mon Sep 17 00:00:00 2001 From: hoangtc Date: Tue, 6 Jun 2017 06:58:07 -0700 Subject: [PATCH 090/220] Fix a minor bug with AdaptiveTrackSelection. When updating track selection, we should only revert back from ideal track selection to current track selection if the currently selected track is not black-listed. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158135644 --- .../exoplayer2/trackselection/AdaptiveTrackSelection.java | 6 +++--- 1 file changed, 3 insertions(+), 3 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 dc78e28e56..50eaaa02e3 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 @@ -156,13 +156,13 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { long nowMs = SystemClock.elapsedRealtime(); // Get the current and ideal selections. int currentSelectedIndex = selectedIndex; - Format currentFormat = getSelectedFormat(); int idealSelectedIndex = determineIdealSelectedIndex(nowMs); - Format idealFormat = getFormat(idealSelectedIndex); // Assume we can switch to the ideal selection. selectedIndex = idealSelectedIndex; // Revert back to the current selection if conditions are not suitable for switching. - if (currentFormat != null && !isBlacklisted(selectedIndex, nowMs)) { + if (!isBlacklisted(currentSelectedIndex, nowMs)) { + Format currentFormat = getFormat(currentSelectedIndex); + Format idealFormat = getFormat(idealSelectedIndex); if (idealFormat.bitrate > currentFormat.bitrate && bufferedDurationUs < minDurationForQualityIncreaseUs) { // The ideal track is a higher quality, but we have insufficient buffer to safely switch From a3ee684e270abe9e271510428dc7851903ed2587 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 6 Jun 2017 07:31:01 -0700 Subject: [PATCH 091/220] Further cleanup of updateSelectedTrack - Return early if the selection is unchanged. - Remove unnecessary variables. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158138187 --- .../AdaptiveTrackSelection.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 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 50eaaa02e3..12f5952dd0 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 @@ -154,23 +154,24 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { @Override public void updateSelectedTrack(long bufferedDurationUs) { long nowMs = SystemClock.elapsedRealtime(); - // Get the current and ideal selections. + // Stash the current selection, then make a new one. int currentSelectedIndex = selectedIndex; - int idealSelectedIndex = determineIdealSelectedIndex(nowMs); - // Assume we can switch to the ideal selection. - selectedIndex = idealSelectedIndex; - // Revert back to the current selection if conditions are not suitable for switching. + selectedIndex = determineIdealSelectedIndex(nowMs); + if (selectedIndex == currentSelectedIndex) { + return; + } if (!isBlacklisted(currentSelectedIndex, nowMs)) { + // Revert back to the current selection if conditions are not suitable for switching. Format currentFormat = getFormat(currentSelectedIndex); - Format idealFormat = getFormat(idealSelectedIndex); - if (idealFormat.bitrate > currentFormat.bitrate + Format selectedFormat = getFormat(selectedIndex); + if (selectedFormat.bitrate > currentFormat.bitrate && bufferedDurationUs < minDurationForQualityIncreaseUs) { - // The ideal track is a higher quality, but we have insufficient buffer to safely switch + // The selected track is a higher quality, but we have insufficient buffer to safely switch // up. Defer switching up for now. selectedIndex = currentSelectedIndex; - } else if (idealFormat.bitrate < currentFormat.bitrate + } else if (selectedFormat.bitrate < currentFormat.bitrate && bufferedDurationUs >= maxDurationForQualityDecreaseUs) { - // The ideal track is a lower quality, but we have sufficient buffer to defer switching + // The selected track is a lower quality, but we have sufficient buffer to defer switching // down for now. selectedIndex = currentSelectedIndex; } From 1637575d4b9d4786784a13b9dead540599ac730a Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 6 Jun 2017 08:03:04 -0700 Subject: [PATCH 092/220] For HLS mode, pick the lowest PID track for each track type This prevents strange behaviors for streams that changes the track declaration order in the PMT. NOTE: This should not change ANY behavior other than the one described above. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158140890 --- .../exoplayer2/extractor/ts/TsExtractor.java | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) 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 71b8375bd8..7b63ce813c 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 @@ -382,10 +382,14 @@ public final class TsExtractor implements Extractor { private static final int TS_PMT_DESC_DVBSUBS = 0x59; private final ParsableBitArray pmtScratch; + private final SparseArray trackIdToReaderScratch; + private final SparseIntArray trackIdToPidScratch; private final int pid; public PmtReader(int pid) { pmtScratch = new ParsableBitArray(new byte[5]); + trackIdToReaderScratch = new SparseArray<>(); + trackIdToPidScratch = new SparseIntArray(); this.pid = pid; } @@ -436,6 +440,8 @@ public final class TsExtractor implements Extractor { new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); } + trackIdToReaderScratch.clear(); + trackIdToPidScratch.clear(); int remainingEntriesLength = sectionData.bytesLeft(); while (remainingEntriesLength > 0) { sectionData.readBytes(pmtScratch, 5); @@ -454,23 +460,30 @@ public final class TsExtractor implements Extractor { if (trackIds.get(trackId)) { continue; } - trackIds.put(trackId, true); - TsPayloadReader reader; - if (mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3) { - reader = id3Reader; - } else { - reader = payloadReaderFactory.createPayloadReader(streamType, esInfo); - if (reader != null) { + TsPayloadReader reader = mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 ? id3Reader + : payloadReaderFactory.createPayloadReader(streamType, esInfo); + if (mode != MODE_HLS + || elementaryPid < trackIdToPidScratch.get(trackId, MAX_PID_PLUS_ONE)) { + trackIdToPidScratch.put(trackId, elementaryPid); + trackIdToReaderScratch.put(trackId, reader); + } + } + + int trackIdCount = trackIdToPidScratch.size(); + for (int i = 0; i < trackIdCount; i++) { + int trackId = trackIdToPidScratch.keyAt(i); + trackIds.put(trackId, true); + TsPayloadReader reader = trackIdToReaderScratch.valueAt(i); + if (reader != null) { + if (reader != id3Reader) { reader.init(timestampAdjuster, output, new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE)); } - } - - if (reader != null) { - tsPayloadReaders.put(elementaryPid, reader); + tsPayloadReaders.put(trackIdToPidScratch.valueAt(i), reader); } } + if (mode == MODE_HLS) { if (!tracksEnded) { output.endTracks(); From 90c62f636ce7e1b27236f76f3bfc596ce6941b23 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 6 Jun 2017 08:15:25 -0700 Subject: [PATCH 093/220] Pass non-null logger into DefaultDrmSessionManager Issue: #2903 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158142226 --- .../android/exoplayer2/demo/PlayerActivity.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 9405a51782..71e5266ef4 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -239,6 +239,13 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay Intent intent = getIntent(); boolean needNewPlayer = player == null; if (needNewPlayer) { + TrackSelection.Factory adaptiveTrackSelectionFactory = + new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); + trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory); + trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory); + lastSeenTrackGroupArray = null; + eventLogger = new EventLogger(trackSelector); + UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA) ? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null; DrmSessionManager drmSessionManager = null; @@ -266,16 +273,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this, drmSessionManager, extensionRendererMode); - TrackSelection.Factory adaptiveTrackSelectionFactory = - new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); - trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory); - trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory); - lastSeenTrackGroupArray = null; - player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); player.addListener(this); - - eventLogger = new EventLogger(trackSelector); player.addListener(eventLogger); player.setAudioDebugListener(eventLogger); player.setVideoDebugListener(eventLogger); From 1316445c00328f9fc04845350800783aef55ba44 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 6 Jun 2017 08:21:38 -0700 Subject: [PATCH 094/220] Constraint buffered percentage to [0,100] Issue: #2902 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158142754 --- .../java/com/google/android/exoplayer2/ExoPlayerImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 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 f0e0ffc9c1..1350c13943 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 @@ -323,10 +323,10 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty()) { return 0; } - long bufferedPosition = getBufferedPosition(); + long position = getBufferedPosition(); long duration = getDuration(); - return (bufferedPosition == C.TIME_UNSET || duration == C.TIME_UNSET) ? 0 - : (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration); + return position == C.TIME_UNSET || duration == C.TIME_UNSET ? 0 + : (duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100)); } @Override From 39b1c85c27aa982aa88707a125d9847378c055d1 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 6 Jun 2017 09:29:24 -0700 Subject: [PATCH 095/220] Expose current scrubber position through onScrubStart Issue: #2910 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158149904 --- .../java/com/google/android/exoplayer2/ui/DefaultTimeBar.java | 2 +- .../com/google/android/exoplayer2/ui/PlaybackControlView.java | 2 +- .../main/java/com/google/android/exoplayer2/ui/TimeBar.java | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) 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 d9754420bf..4ede786175 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 @@ -442,7 +442,7 @@ public class DefaultTimeBar extends View implements TimeBar { parent.requestDisallowInterceptTouchEvent(true); } if (listener != null) { - listener.onScrubStart(this); + listener.onScrubStart(this, getScrubberPosition()); } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index d0f8c33b58..2bd576d32e 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -1045,7 +1045,7 @@ public class PlaybackControlView extends FrameLayout { OnClickListener { @Override - public void onScrubStart(TimeBar timeBar) { + public void onScrubStart(TimeBar timeBar, long position) { removeCallbacks(hideAction); scrubbing = true; } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java index aeb8e0255e..2fd5bff5eb 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java @@ -95,8 +95,9 @@ public interface TimeBar { * Called when the user starts moving the scrubber. * * @param timeBar The time bar. + * @param position The position of the scrubber, in milliseconds. */ - void onScrubStart(TimeBar timeBar); + void onScrubStart(TimeBar timeBar, long position); /** * Called when the user moves the scrubber. From 2439c582d408103b4360e0aed3cbe63a2057c492 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 6 Jun 2017 10:03:08 -0700 Subject: [PATCH 096/220] Bump version + update release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158153988 --- RELEASENOTES.md | 14 ++++++++++++++ build.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 ++-- .../android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6a1defa809..4f147e2bbd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,19 @@ # Release notes # +### r2.4.2 ### + +* Stability: Work around Nexus 10 reboot when playing certain content + ([2806](https://github.com/google/ExoPlayer/issues/2806)). +* MP3: Correctly treat MP3s with INFO headers as constant bitrate + ([2895](https://github.com/google/ExoPlayer/issues/2895)). +* HLS: Use average rather than peak bandwidth when available + ([#2863](https://github.com/google/ExoPlayer/issues/2863)). +* SmoothStreaming: Fix timeline for live streams + ([#2760](https://github.com/google/ExoPlayer/issues/2760)). +* UI: Fix DefaultTimeBar invalidation + ([#2871](https://github.com/google/ExoPlayer/issues/2871)). +* Misc bugfixes. + ### r2.4.1 ### * Stability: Avoid OutOfMemoryError in extractors when parsing malformed media diff --git a/build.gradle b/build.gradle index 258b11d2e6..4f18e7c801 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ allprojects { releaseRepoName = getBintrayRepo() releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.4.1' + releaseVersion = 'r2.4.2' releaseWebsite = 'https://github.com/google/ExoPlayer' } if (it.hasProperty('externalBuildDir')) { diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 1bb859028d..34256d41c1 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2402" + android:versionName="2.4.2"> 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 23c2ddbde9..c6fc139208 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 @@ -24,13 +24,13 @@ public interface 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. - String VERSION = "2.4.1"; + String VERSION = "2.4.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. - String VERSION_SLASHY = "ExoPlayerLib/2.4.1"; + String VERSION_SLASHY = "ExoPlayerLib/2.4.2"; /** * The version of the library expressed as an integer, for example 1002003. @@ -40,7 +40,7 @@ public interface ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - int VERSION_INT = 2004001; + int VERSION_INT = 2004002; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From c24ef592e77d6146983a3cf156e86f14dfcecd07 Mon Sep 17 00:00:00 2001 From: Kiall Mac Innes Date: Sun, 30 Apr 2017 12:37:15 +0100 Subject: [PATCH 097/220] Include Pixel Aspect Ratio in DebugTextViewHelper Add the video Pixel Aspect Ratio to the DebugTextViewHelper in order to help debug issues related to PAR changes etc --- .../android/exoplayer2/ui/DebugTextViewHelper.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index 68fa6a8cc9..e65b475c97 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -163,9 +163,15 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe if (format == null) { return ""; } + float par = format.pixelWidthHeightRatio; + String parInfo = ""; + if (par != Format.NO_VALUE && (int) par != 1) { + // Add pixel aspect ratio only when it's useful + parInfo = " par:" + format.pixelWidthHeightRatio; + } return "\n" + format.sampleMimeType + "(id:" + format.id + " r:" + format.width + "x" - + format.height + getDecoderCountersBufferCountString(player.getVideoDecoderCounters()) - + ")"; + + format.height + parInfo + + getDecoderCountersBufferCountString(player.getVideoDecoderCounters()) + ")"; } private String getAudioString() { From cdad6a4ef184995178091e9c16f45438f95b4c7e Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 6 Jun 2017 17:35:25 -0700 Subject: [PATCH 098/220] Move playback test utils to testutils. This allows other tests to reuse the util classes without having to link to playbacktests. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158214560 --- library/core/build.gradle | 1 + library/dash/build.gradle | 7 +------ library/smoothstreaming/build.gradle | 7 +------ playbacktests/build.gradle | 3 ++- playbacktests/src/androidTest/AndroidManifest.xml | 2 +- .../playbacktests/gts/DashStreamingTest.java | 4 ++-- .../playbacktests/gts/DashTestRunner.java | 14 +++++++------- .../playbacktests/gts/DashWidevineOfflineTest.java | 4 ++-- testutils/src/main/AndroidManifest.xml | 2 +- .../android/exoplayer2/testutil}/Action.java | 2 +- .../exoplayer2/testutil}/ActionSchedule.java | 14 +++++++------- .../testutil}/DebugRenderersFactory.java | 2 +- .../exoplayer2/testutil}/DecoderCountersUtil.java | 2 +- .../exoplayer2/testutil}/ExoHostedTest.java | 4 ++-- .../android/exoplayer2/testutil}/HostActivity.java | 8 ++++---- .../exoplayer2/testutil}/LogcatMetricsLogger.java | 2 +- .../exoplayer2/testutil}/MetricsLogger.java | 2 +- .../src/main/res/layout/host_activity.xml | 0 18 files changed, 36 insertions(+), 44 deletions(-) rename {playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util => testutils/src/main/java/com/google/android/exoplayer2/testutil}/Action.java (98%) rename {playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util => testutils/src/main/java/com/google/android/exoplayer2/testutil}/ActionSchedule.java (92%) rename {playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util => testutils/src/main/java/com/google/android/exoplayer2/testutil}/DebugRenderersFactory.java (98%) rename {playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util => testutils/src/main/java/com/google/android/exoplayer2/testutil}/DecoderCountersUtil.java (97%) rename {playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util => testutils/src/main/java/com/google/android/exoplayer2/testutil}/ExoHostedTest.java (98%) rename {playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util => testutils/src/main/java/com/google/android/exoplayer2/testutil}/HostActivity.java (96%) rename {playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util => testutils/src/main/java/com/google/android/exoplayer2/testutil}/LogcatMetricsLogger.java (95%) rename {playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util => testutils/src/main/java/com/google/android/exoplayer2/testutil}/MetricsLogger.java (97%) rename {playbacktests => testutils}/src/main/res/layout/host_activity.xml (100%) diff --git a/library/core/build.gradle b/library/core/build.gradle index bb0adaa4c7..ad1c150fe7 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -22,6 +22,7 @@ android { targetSdkVersion project.ext.targetSdkVersion } + // Workaround to prevent circular dependency on project :testutils. sourceSets { androidTest { java.srcDirs += "../../testutils/src/main/java/" diff --git a/library/dash/build.gradle b/library/dash/build.gradle index ebad5a8603..36d3edfbae 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -22,12 +22,6 @@ android { targetSdkVersion project.ext.targetSdkVersion } - sourceSets { - androidTest { - java.srcDirs += "../../testutils/src/main/java/" - } - } - buildTypes { debug { testCoverageEnabled = true @@ -39,6 +33,7 @@ dependencies { compile project(':library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion compile 'com.android.support:support-core-utils:' + supportLibraryVersion + androidTestCompile project(':testutils') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index 81f8234672..28ebd74758 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -22,12 +22,6 @@ android { targetSdkVersion project.ext.targetSdkVersion } - sourceSets { - androidTest { - java.srcDirs += "../../testutils/src/main/java/" - } - } - buildTypes { debug { testCoverageEnabled = true @@ -38,6 +32,7 @@ android { dependencies { compile project(':library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion + androidTestCompile project(':testutils') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index 6a09eac49e..199077f2b2 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -24,7 +24,8 @@ android { } dependencies { - compile project(':library-core') + androidTestCompile project(':library-core') androidTestCompile project(':library-dash') androidTestCompile project(':library-hls') + androidTestCompile project(':testutils') } diff --git a/playbacktests/src/androidTest/AndroidManifest.xml b/playbacktests/src/androidTest/AndroidManifest.xml index 64b927655f..053fe4e61c 100644 --- a/playbacktests/src/androidTest/AndroidManifest.xml +++ b/playbacktests/src/androidTest/AndroidManifest.xml @@ -28,7 +28,7 @@ tools:ignore="MissingApplicationIcon,HardcodedDebugMode"> - diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java index 24f73e9d08..669241e65c 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java @@ -20,8 +20,8 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; -import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; -import com.google.android.exoplayer2.playbacktests.util.HostActivity; +import com.google.android.exoplayer2.testutil.ActionSchedule; +import com.google.android.exoplayer2.testutil.HostActivity; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; 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 381a873d94..7d80acd9e4 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 @@ -38,18 +38,18 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.MediaDrmCallback; import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; -import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; -import com.google.android.exoplayer2.playbacktests.util.DebugRenderersFactory; -import com.google.android.exoplayer2.playbacktests.util.DecoderCountersUtil; -import com.google.android.exoplayer2.playbacktests.util.ExoHostedTest; -import com.google.android.exoplayer2.playbacktests.util.HostActivity; -import com.google.android.exoplayer2.playbacktests.util.HostActivity.HostedTest; -import com.google.android.exoplayer2.playbacktests.util.MetricsLogger; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.testutil.ActionSchedule; +import com.google.android.exoplayer2.testutil.DebugRenderersFactory; +import com.google.android.exoplayer2.testutil.DecoderCountersUtil; +import com.google.android.exoplayer2.testutil.ExoHostedTest; +import com.google.android.exoplayer2.testutil.HostActivity; +import com.google.android.exoplayer2.testutil.HostActivity.HostedTest; +import com.google.android.exoplayer2.testutil.MetricsLogger; import com.google.android.exoplayer2.trackselection.FixedTrackSelection; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.RandomTrackSelection; diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java index 44f25b49d9..e43eab5dc3 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java @@ -22,10 +22,10 @@ import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.OfflineLicenseHelper; -import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; -import com.google.android.exoplayer2.playbacktests.util.HostActivity; import com.google.android.exoplayer2.source.dash.DashUtil; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.testutil.ActionSchedule; +import com.google.android.exoplayer2.testutil.HostActivity; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.util.MimeTypes; diff --git a/testutils/src/main/AndroidManifest.xml b/testutils/src/main/AndroidManifest.xml index 31db3e2f12..ef1411d737 100644 --- a/testutils/src/main/AndroidManifest.xml +++ b/testutils/src/main/AndroidManifest.xml @@ -14,4 +14,4 @@ limitations under the License. --> - + diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java similarity index 98% rename from playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/Action.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index edb0fa59bf..b1c6f081cf 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.playbacktests.util; +package com.google.android.exoplayer2.testutil; import android.util.Log; import android.view.Surface; diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java similarity index 92% rename from playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ActionSchedule.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 94204ca1b5..ede4dc5553 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.playbacktests.util; +package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.view.Surface; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.playbacktests.util.Action.ClearVideoSurface; -import com.google.android.exoplayer2.playbacktests.util.Action.Seek; -import com.google.android.exoplayer2.playbacktests.util.Action.SetPlayWhenReady; -import com.google.android.exoplayer2.playbacktests.util.Action.SetRendererDisabled; -import com.google.android.exoplayer2.playbacktests.util.Action.SetVideoSurface; -import com.google.android.exoplayer2.playbacktests.util.Action.Stop; +import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface; +import com.google.android.exoplayer2.testutil.Action.Seek; +import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; +import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; +import com.google.android.exoplayer2.testutil.Action.SetVideoSurface; +import com.google.android.exoplayer2.testutil.Action.Stop; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; /** diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java similarity index 98% rename from playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugRenderersFactory.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index 6cb7673ebd..af7c1a3e2a 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.playbacktests.util; +package com.google.android.exoplayer2.testutil; import android.annotation.TargetApi; import android.content.Context; diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DecoderCountersUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DecoderCountersUtil.java similarity index 97% rename from playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DecoderCountersUtil.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/DecoderCountersUtil.java index aafb828345..448ec79c2d 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DecoderCountersUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DecoderCountersUtil.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.playbacktests.util; +package com.google.android.exoplayer2.testutil; import com.google.android.exoplayer2.decoder.DecoderCounters; import junit.framework.TestCase; diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java similarity index 98% rename from playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index 56f11d86e3..7af3e990af 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.playbacktests.util; +package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.os.SystemClock; @@ -33,9 +33,9 @@ import com.google.android.exoplayer2.audio.AudioTrack; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; -import com.google.android.exoplayer2.playbacktests.util.HostActivity.HostedTest; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; +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; diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/HostActivity.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java similarity index 96% rename from playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/HostActivity.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java index 9c2ced3a8a..ecbe00b487 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/HostActivity.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.playbacktests.util; +package com.google.android.exoplayer2.testutil; import static junit.framework.Assert.fail; @@ -32,7 +32,6 @@ import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.Window; -import com.google.android.exoplayer2.playbacktests.R; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -142,8 +141,9 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); - setContentView(R.layout.host_activity); - surfaceView = (SurfaceView) findViewById(R.id.surface_view); + setContentView(getResources().getIdentifier("host_activity", "layout", getPackageName())); + surfaceView = (SurfaceView) findViewById( + getResources().getIdentifier("surface_view", "id", getPackageName())); surfaceView.getHolder().addCallback(this); mainHandler = new Handler(); checkCanStopRunnable = new CheckCanStopRunnable(); diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/LogcatMetricsLogger.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/LogcatMetricsLogger.java similarity index 95% rename from playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/LogcatMetricsLogger.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/LogcatMetricsLogger.java index 4c44f77143..fdff47dd2c 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/LogcatMetricsLogger.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/LogcatMetricsLogger.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.playbacktests.util; +package com.google.android.exoplayer2.testutil; import android.util.Log; diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/MetricsLogger.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MetricsLogger.java similarity index 97% rename from playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/MetricsLogger.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/MetricsLogger.java index 6e36ff728f..64d1944927 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/MetricsLogger.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MetricsLogger.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.playbacktests.util; +package com.google.android.exoplayer2.testutil; import android.app.Instrumentation; diff --git a/playbacktests/src/main/res/layout/host_activity.xml b/testutils/src/main/res/layout/host_activity.xml similarity index 100% rename from playbacktests/src/main/res/layout/host_activity.xml rename to testutils/src/main/res/layout/host_activity.xml From 5908d2d7e2e92fa6143034886a351248bf98f1d8 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 7 Jun 2017 08:01:28 -0700 Subject: [PATCH 099/220] Update handled schemes for timing element resolution. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158269487 --- .../exoplayer2/source/dash/DashMediaSource.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 a469f0aae8..111729d361 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 @@ -410,12 +410,14 @@ public final class DashMediaSource implements MediaSource { private void resolveUtcTimingElement(UtcTimingElement timingElement) { String scheme = timingElement.schemeIdUri; - if (Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2012")) { + if (Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2014") + || Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2012")) { resolveUtcTimingElementDirect(timingElement); - } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2014")) { + } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2014") + || Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2012")) { resolveUtcTimingElementHttp(timingElement, new Iso8601Parser()); - } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012") - || Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014")) { + } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014") + || Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012")) { resolveUtcTimingElementHttp(timingElement, new XsDateTimeParser()); } else { // Unsupported scheme. From 1b06ce740734e60d6a78f65720d84daf367cbc78 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 7 Jun 2017 08:02:57 -0700 Subject: [PATCH 100/220] Fix ArrayIndexOutOfBoundsException in DashMediaPeriod ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158269662 --- .../google/android/exoplayer2/source/dash/DashMediaPeriod.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 0d6b7e28ef..6b9668e4b9 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 @@ -273,7 +273,7 @@ import java.util.List; AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]); int primaryTrackGroupIndex = trackGroupCount; boolean hasEventMessageTrack = primaryGroupHasEventMessageTrackFlags[i]; - boolean hasCea608Track = primaryGroupHasEventMessageTrackFlags[i]; + boolean hasCea608Track = primaryGroupHasCea608TrackFlags[i]; trackGroups[trackGroupCount] = new TrackGroup(formats); trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(firstAdaptationSet.type, From b7b0fef694a1a4477de11d40193f9c7645b695f2 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 8 Jun 2017 07:44:25 -0700 Subject: [PATCH 101/220] Split InfoQueue into its own class It's pretty big as an inner class, and is going to get a little more complicated. I think it makes sense to be able to consider it in isolation. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158393754 --- .../extractor/DefaultTrackOutput.java | 453 +----------------- .../extractor/SampleMetadataQueue.java | 426 ++++++++++++++++ 2 files changed, 451 insertions(+), 428 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/SampleMetadataQueue.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java index c879d8e695..09970aaff0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java @@ -19,11 +19,10 @@ 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.SampleMetadataQueue.SampleExtrasHolder; import com.google.android.exoplayer2.upstream.Allocation; import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; import java.nio.ByteBuffer; @@ -59,9 +58,9 @@ public final class DefaultTrackOutput implements TrackOutput { private final Allocator allocator; private final int allocationLength; - private final InfoQueue infoQueue; + private final SampleMetadataQueue metadataQueue; private final LinkedBlockingDeque dataQueue; - private final BufferExtrasHolder extrasHolder; + private final SampleExtrasHolder extrasHolder; private final ParsableByteArray scratch; private final AtomicInteger state; @@ -85,9 +84,9 @@ public final class DefaultTrackOutput implements TrackOutput { public DefaultTrackOutput(Allocator allocator) { this.allocator = allocator; allocationLength = allocator.getIndividualAllocationLength(); - infoQueue = new InfoQueue(); + metadataQueue = new SampleMetadataQueue(); dataQueue = new LinkedBlockingDeque<>(); - extrasHolder = new BufferExtrasHolder(); + extrasHolder = new SampleExtrasHolder(); scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); state = new AtomicInteger(); lastAllocationOffset = allocationLength; @@ -103,7 +102,7 @@ public final class DefaultTrackOutput implements TrackOutput { public void reset(boolean enable) { int previousState = state.getAndSet(enable ? STATE_ENABLED : STATE_DISABLED); clearSampleData(); - infoQueue.resetLargestParsedTimestamps(); + metadataQueue.resetLargestParsedTimestamps(); if (previousState == STATE_DISABLED) { downstreamFormat = null; } @@ -115,7 +114,7 @@ public final class DefaultTrackOutput implements TrackOutput { * @param sourceId The source identifier. */ public void sourceId(int sourceId) { - infoQueue.sourceId(sourceId); + metadataQueue.sourceId(sourceId); } /** @@ -130,7 +129,7 @@ public final class DefaultTrackOutput implements TrackOutput { * Returns the current absolute write index. */ public int getWriteIndex() { - return infoQueue.getWriteIndex(); + return metadataQueue.getWriteIndex(); } /** @@ -139,7 +138,7 @@ public final class DefaultTrackOutput implements TrackOutput { * @param discardFromIndex The absolute index of the first sample to be discarded. */ public void discardUpstreamSamples(int discardFromIndex) { - totalBytesWritten = infoQueue.discardUpstreamSamples(discardFromIndex); + totalBytesWritten = metadataQueue.discardUpstreamSamples(discardFromIndex); dropUpstreamFrom(totalBytesWritten); } @@ -184,14 +183,14 @@ public final class DefaultTrackOutput implements TrackOutput { * Returns whether the buffer is empty. */ public boolean isEmpty() { - return infoQueue.isEmpty(); + return metadataQueue.isEmpty(); } /** * Returns the current absolute read index. */ public int getReadIndex() { - return infoQueue.getReadIndex(); + return metadataQueue.getReadIndex(); } /** @@ -201,14 +200,14 @@ public final class DefaultTrackOutput implements TrackOutput { * @return The source id. */ public int peekSourceId() { - return infoQueue.peekSourceId(); + return metadataQueue.peekSourceId(); } /** * Returns the upstream {@link Format} in which samples are being queued. */ public Format getUpstreamFormat() { - return infoQueue.getUpstreamFormat(); + return metadataQueue.getUpstreamFormat(); } /** @@ -222,14 +221,14 @@ public final class DefaultTrackOutput implements TrackOutput { * samples have been queued. */ public long getLargestQueuedTimestampUs() { - return infoQueue.getLargestQueuedTimestampUs(); + return metadataQueue.getLargestQueuedTimestampUs(); } /** * Skips all samples currently in the buffer. */ public void skipAll() { - long nextOffset = infoQueue.skipAll(); + long nextOffset = metadataQueue.skipAll(); if (nextOffset != C.POSITION_UNSET) { dropDownstreamTo(nextOffset); } @@ -247,7 +246,7 @@ public final class DefaultTrackOutput implements TrackOutput { * @return Whether the skip was successful. */ public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) { - long nextOffset = infoQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer); + long nextOffset = metadataQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer); if (nextOffset == C.POSITION_UNSET) { return false; } @@ -273,7 +272,7 @@ public final class DefaultTrackOutput implements TrackOutput { */ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, boolean loadingFinished, long decodeOnlyUntilUs) { - int result = infoQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, + int result = metadataQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, downstreamFormat, extrasHolder); switch (result) { case C.RESULT_FORMAT_READ: @@ -306,13 +305,13 @@ public final class DefaultTrackOutput implements TrackOutput { * Reads encryption data for the current sample. *

      * The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and - * {@link BufferExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The - * same value is added to {@link BufferExtrasHolder#offset}. + * {@link SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The + * same value is added to {@link SampleExtrasHolder#offset}. * * @param buffer The buffer into which the encryption data should be written. * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. */ - private void readEncryptionData(DecoderInputBuffer buffer, BufferExtrasHolder extrasHolder) { + private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { long offset = extrasHolder.offset; // Read the signal byte. @@ -459,7 +458,7 @@ public final class DefaultTrackOutput implements TrackOutput { @Override public void format(Format format) { Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs); - boolean formatChanged = infoQueue.format(adjustedFormat); + boolean formatChanged = metadataQueue.format(adjustedFormat); lastUnadjustedFormat = format; pendingFormatAdjustment = false; if (upstreamFormatChangeListener != null && formatChanged) { @@ -522,19 +521,19 @@ public final class DefaultTrackOutput implements TrackOutput { format(lastUnadjustedFormat); } if (!startWriteOperation()) { - infoQueue.commitSampleTimestamp(timeUs); + metadataQueue.commitSampleTimestamp(timeUs); return; } try { if (pendingSplice) { - if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !infoQueue.attemptSplice(timeUs)) { + if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !metadataQueue.attemptSplice(timeUs)) { return; } pendingSplice = false; } timeUs += sampleOffsetUs; long absoluteOffset = totalBytesWritten - size - offset; - infoQueue.commitSample(timeUs, flags, absoluteOffset, size, cryptoData); + metadataQueue.commitSample(timeUs, flags, absoluteOffset, size, cryptoData); } finally { endWriteOperation(); } @@ -553,7 +552,7 @@ public final class DefaultTrackOutput implements TrackOutput { } private void clearSampleData() { - infoQueue.clearSampleData(); + metadataQueue.clearSampleData(); allocator.release(dataQueue.toArray(new Allocation[dataQueue.size()])); dataQueue.clear(); allocator.trim(); @@ -593,406 +592,4 @@ public final class DefaultTrackOutput implements TrackOutput { return format; } - /** - * Holds information about the samples in the rolling buffer. - */ - private static final class InfoQueue { - - private static final int SAMPLE_CAPACITY_INCREMENT = 1000; - - private int capacity; - - private int[] sourceIds; - private long[] offsets; - private int[] sizes; - private int[] flags; - private long[] timesUs; - private CryptoData[] cryptoDatas; - private Format[] formats; - - private int queueSize; - private int absoluteReadIndex; - private int relativeReadIndex; - private int relativeWriteIndex; - - private long largestDequeuedTimestampUs; - private long largestQueuedTimestampUs; - private boolean upstreamKeyframeRequired; - private boolean upstreamFormatRequired; - private Format upstreamFormat; - private int upstreamSourceId; - - public InfoQueue() { - capacity = SAMPLE_CAPACITY_INCREMENT; - sourceIds = new int[capacity]; - offsets = new long[capacity]; - timesUs = new long[capacity]; - flags = new int[capacity]; - sizes = new int[capacity]; - cryptoDatas = new CryptoData[capacity]; - formats = new Format[capacity]; - largestDequeuedTimestampUs = Long.MIN_VALUE; - largestQueuedTimestampUs = Long.MIN_VALUE; - upstreamFormatRequired = true; - upstreamKeyframeRequired = true; - } - - public void clearSampleData() { - absoluteReadIndex = 0; - relativeReadIndex = 0; - relativeWriteIndex = 0; - queueSize = 0; - upstreamKeyframeRequired = true; - } - - // Called by the consuming thread, but only when there is no loading thread. - - public void resetLargestParsedTimestamps() { - largestDequeuedTimestampUs = Long.MIN_VALUE; - largestQueuedTimestampUs = Long.MIN_VALUE; - } - - /** - * Returns the current absolute write index. - */ - public int getWriteIndex() { - return absoluteReadIndex + queueSize; - } - - /** - * Discards samples from the write side of the buffer. - * - * @param discardFromIndex The absolute index of the first sample to be discarded. - * @return The reduced total number of bytes written, after the samples have been discarded. - */ - public long discardUpstreamSamples(int discardFromIndex) { - int discardCount = getWriteIndex() - discardFromIndex; - Assertions.checkArgument(0 <= discardCount && discardCount <= queueSize); - - if (discardCount == 0) { - if (absoluteReadIndex == 0) { - // queueSize == absoluteReadIndex == 0, so nothing has been written to the queue. - return 0; - } - int lastWriteIndex = (relativeWriteIndex == 0 ? capacity : relativeWriteIndex) - 1; - return offsets[lastWriteIndex] + sizes[lastWriteIndex]; - } - - queueSize -= discardCount; - relativeWriteIndex = (relativeWriteIndex + capacity - discardCount) % capacity; - // Update the largest queued timestamp, assuming that the timestamps prior to a keyframe are - // always less than the timestamp of the keyframe itself, and of subsequent frames. - largestQueuedTimestampUs = Long.MIN_VALUE; - for (int i = queueSize - 1; i >= 0; i--) { - int sampleIndex = (relativeReadIndex + i) % capacity; - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timesUs[sampleIndex]); - if ((flags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { - break; - } - } - return offsets[relativeWriteIndex]; - } - - public void sourceId(int sourceId) { - upstreamSourceId = sourceId; - } - - // Called by the consuming thread. - - /** - * Returns the current absolute read index. - */ - public int getReadIndex() { - return absoluteReadIndex; - } - - /** - * Peeks the source id of the next sample, or the current upstream source id if the queue is - * empty. - */ - public int peekSourceId() { - return queueSize == 0 ? upstreamSourceId : sourceIds[relativeReadIndex]; - } - - /** - * Returns whether the queue is empty. - */ - public synchronized boolean isEmpty() { - return queueSize == 0; - } - - /** - * Returns the upstream {@link Format} in which samples are being queued. - */ - public synchronized Format getUpstreamFormat() { - return upstreamFormatRequired ? null : upstreamFormat; - } - - /** - * Returns the largest sample timestamp that has been queued since the last {@link #reset}. - *

      - * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not - * considered as having been queued. Samples that were dequeued from the front of the queue are - * considered as having been queued. - * - * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no - * samples have been queued. - */ - public synchronized long getLargestQueuedTimestampUs() { - return Math.max(largestDequeuedTimestampUs, largestQueuedTimestampUs); - } - - /** - * Attempts to read from the queue. - * - * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. - * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the - * end of the stream. If a sample is read then the buffer is populated with information - * about the sample, but not its data. The size and absolute position of the data in the - * rolling buffer is stored in {@code extrasHolder}, along with an encryption id if present - * and the absolute position of the first byte that may still be required after the current - * sample has been read. May be null if the caller requires that the format of the stream be - * read even if it's not changing. - * @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 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. - * @param extrasHolder The holder into which extra sample information should be written. - * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} - * or {@link C#RESULT_BUFFER_READ}. - */ - @SuppressWarnings("ReferenceEquality") - public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, - boolean formatRequired, boolean loadingFinished, Format downstreamFormat, - BufferExtrasHolder extrasHolder) { - if (queueSize == 0) { - if (loadingFinished) { - buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); - return C.RESULT_BUFFER_READ; - } else if (upstreamFormat != null - && (formatRequired || upstreamFormat != downstreamFormat)) { - formatHolder.format = upstreamFormat; - return C.RESULT_FORMAT_READ; - } else { - return C.RESULT_NOTHING_READ; - } - } - - if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { - formatHolder.format = formats[relativeReadIndex]; - return C.RESULT_FORMAT_READ; - } - - if (buffer.isFlagsOnly()) { - return C.RESULT_NOTHING_READ; - } - - buffer.timeUs = timesUs[relativeReadIndex]; - buffer.setFlags(flags[relativeReadIndex]); - extrasHolder.size = sizes[relativeReadIndex]; - extrasHolder.offset = offsets[relativeReadIndex]; - extrasHolder.cryptoData = cryptoDatas[relativeReadIndex]; - - largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, buffer.timeUs); - queueSize--; - relativeReadIndex++; - absoluteReadIndex++; - if (relativeReadIndex == capacity) { - // Wrap around. - relativeReadIndex = 0; - } - - extrasHolder.nextOffset = queueSize > 0 ? offsets[relativeReadIndex] - : extrasHolder.offset + extrasHolder.size; - return C.RESULT_BUFFER_READ; - } - - /** - * Skips all samples in the buffer. - * - * @return The offset up to which data should be dropped, or {@link C#POSITION_UNSET} if no - * dropping of data is required. - */ - public synchronized long skipAll() { - if (queueSize == 0) { - return C.POSITION_UNSET; - } - - int lastSampleIndex = (relativeReadIndex + queueSize - 1) % capacity; - relativeReadIndex = (relativeReadIndex + queueSize) % capacity; - absoluteReadIndex += queueSize; - queueSize = 0; - return offsets[lastSampleIndex] + sizes[lastSampleIndex]; - } - - /** - * Attempts to locate the keyframe before or at the specified time. If - * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs} - * falls within the buffer. - * - * @param timeUs The seek time. - * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end - * of the buffer. - * @return The offset of the keyframe's data if the keyframe was present. - * {@link C#POSITION_UNSET} otherwise. - */ - public synchronized long skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) { - if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) { - return C.POSITION_UNSET; - } - - if (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer) { - return C.POSITION_UNSET; - } - - // This could be optimized to use a binary search, however in practice callers to this method - // often pass times near to the start of the buffer. Hence it's unclear whether switching to - // a binary search would yield any real benefit. - int sampleCount = 0; - int sampleCountToKeyframe = -1; - int searchIndex = relativeReadIndex; - while (searchIndex != relativeWriteIndex) { - if (timesUs[searchIndex] > timeUs) { - // We've gone too far. - break; - } else if ((flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { - // We've found a keyframe, and we're still before the seek position. - sampleCountToKeyframe = sampleCount; - } - searchIndex = (searchIndex + 1) % capacity; - sampleCount++; - } - - if (sampleCountToKeyframe == -1) { - return C.POSITION_UNSET; - } - - relativeReadIndex = (relativeReadIndex + sampleCountToKeyframe) % capacity; - absoluteReadIndex += sampleCountToKeyframe; - queueSize -= sampleCountToKeyframe; - return offsets[relativeReadIndex]; - } - - // Called by the loading thread. - - public synchronized boolean format(Format format) { - if (format == null) { - upstreamFormatRequired = true; - return false; - } - 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; - return true; - } - } - - public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset, - int size, CryptoData cryptoData) { - if (upstreamKeyframeRequired) { - if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) { - return; - } - upstreamKeyframeRequired = false; - } - Assertions.checkState(!upstreamFormatRequired); - commitSampleTimestamp(timeUs); - timesUs[relativeWriteIndex] = timeUs; - offsets[relativeWriteIndex] = offset; - sizes[relativeWriteIndex] = size; - flags[relativeWriteIndex] = sampleFlags; - cryptoDatas[relativeWriteIndex] = cryptoData; - formats[relativeWriteIndex] = upstreamFormat; - sourceIds[relativeWriteIndex] = upstreamSourceId; - // Increment the write index. - queueSize++; - if (queueSize == capacity) { - // Increase the capacity. - int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT; - int[] newSourceIds = new int[newCapacity]; - long[] newOffsets = new long[newCapacity]; - long[] newTimesUs = new long[newCapacity]; - int[] newFlags = new int[newCapacity]; - int[] newSizes = new int[newCapacity]; - CryptoData[] newCryptoDatas = new CryptoData[newCapacity]; - Format[] newFormats = new Format[newCapacity]; - int beforeWrap = capacity - relativeReadIndex; - System.arraycopy(offsets, relativeReadIndex, newOffsets, 0, beforeWrap); - System.arraycopy(timesUs, relativeReadIndex, newTimesUs, 0, beforeWrap); - System.arraycopy(flags, relativeReadIndex, newFlags, 0, beforeWrap); - System.arraycopy(sizes, relativeReadIndex, newSizes, 0, beforeWrap); - System.arraycopy(cryptoDatas, relativeReadIndex, newCryptoDatas, 0, beforeWrap); - System.arraycopy(formats, relativeReadIndex, newFormats, 0, beforeWrap); - System.arraycopy(sourceIds, relativeReadIndex, newSourceIds, 0, beforeWrap); - int afterWrap = relativeReadIndex; - System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap); - System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap); - System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap); - System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap); - System.arraycopy(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap); - System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap); - System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap); - offsets = newOffsets; - timesUs = newTimesUs; - flags = newFlags; - sizes = newSizes; - cryptoDatas = newCryptoDatas; - formats = newFormats; - sourceIds = newSourceIds; - relativeReadIndex = 0; - relativeWriteIndex = capacity; - queueSize = capacity; - capacity = newCapacity; - } else { - relativeWriteIndex++; - if (relativeWriteIndex == capacity) { - // Wrap around. - relativeWriteIndex = 0; - } - } - } - - public synchronized void commitSampleTimestamp(long timeUs) { - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); - } - - /** - * Attempts to discard samples from the tail of the queue to allow samples starting from the - * specified timestamp to be spliced in. - * - * @param timeUs The timestamp at which the splice occurs. - * @return Whether the splice was successful. - */ - public synchronized boolean attemptSplice(long timeUs) { - if (largestDequeuedTimestampUs >= timeUs) { - return false; - } - int retainCount = queueSize; - while (retainCount > 0 - && timesUs[(relativeReadIndex + retainCount - 1) % capacity] >= timeUs) { - retainCount--; - } - discardUpstreamSamples(absoluteReadIndex + retainCount); - return true; - } - - } - - /** - * Holds additional buffer information not held by {@link DecoderInputBuffer}. - */ - private static final class BufferExtrasHolder { - - public int size; - public long offset; - public long nextOffset; - public CryptoData cryptoData; - - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SampleMetadataQueue.java new file mode 100644 index 0000000000..9802392913 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SampleMetadataQueue.java @@ -0,0 +1,426 @@ +/* + * 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.extractor; + +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.CryptoData; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +/** + * A queue of metadata describing the contents of a media buffer. + */ +/* package */ final class SampleMetadataQueue { + + /** + * A holder for sample metadata not held by {@link DecoderInputBuffer}. + */ + public static final class SampleExtrasHolder { + + public int size; + public long offset; + public long nextOffset; + public CryptoData cryptoData; + + } + + private static final int SAMPLE_CAPACITY_INCREMENT = 1000; + + private int capacity; + private int[] sourceIds; + private long[] offsets; + private int[] sizes; + private int[] flags; + private long[] timesUs; + private CryptoData[] cryptoDatas; + private Format[] formats; + + private int queueSize; + private int absoluteReadIndex; + private int relativeReadIndex; + private int relativeWriteIndex; + + private long largestDequeuedTimestampUs; + private long largestQueuedTimestampUs; + private boolean upstreamKeyframeRequired; + private boolean upstreamFormatRequired; + private Format upstreamFormat; + private int upstreamSourceId; + + public SampleMetadataQueue() { + capacity = SAMPLE_CAPACITY_INCREMENT; + sourceIds = new int[capacity]; + offsets = new long[capacity]; + timesUs = new long[capacity]; + flags = new int[capacity]; + sizes = new int[capacity]; + cryptoDatas = new CryptoData[capacity]; + formats = new Format[capacity]; + largestDequeuedTimestampUs = Long.MIN_VALUE; + largestQueuedTimestampUs = Long.MIN_VALUE; + upstreamFormatRequired = true; + upstreamKeyframeRequired = true; + } + + public void clearSampleData() { + absoluteReadIndex = 0; + relativeReadIndex = 0; + relativeWriteIndex = 0; + queueSize = 0; + upstreamKeyframeRequired = true; + } + + // Called by the consuming thread, but only when there is no loading thread. + + public void resetLargestParsedTimestamps() { + largestDequeuedTimestampUs = Long.MIN_VALUE; + largestQueuedTimestampUs = Long.MIN_VALUE; + } + + /** + * Returns the current absolute write index. + */ + public int getWriteIndex() { + return absoluteReadIndex + queueSize; + } + + /** + * Discards samples from the write side of the buffer. + * + * @param discardFromIndex The absolute index of the first sample to be discarded. + * @return The reduced total number of bytes written, after the samples have been discarded. + */ + public long discardUpstreamSamples(int discardFromIndex) { + int discardCount = getWriteIndex() - discardFromIndex; + Assertions.checkArgument(0 <= discardCount && discardCount <= queueSize); + + if (discardCount == 0) { + if (absoluteReadIndex == 0) { + // queueSize == absoluteReadIndex == 0, so nothing has been written to the queue. + return 0; + } + int lastWriteIndex = (relativeWriteIndex == 0 ? capacity : relativeWriteIndex) - 1; + return offsets[lastWriteIndex] + sizes[lastWriteIndex]; + } + + queueSize -= discardCount; + relativeWriteIndex = (relativeWriteIndex + capacity - discardCount) % capacity; + // Update the largest queued timestamp, assuming that the timestamps prior to a keyframe are + // always less than the timestamp of the keyframe itself, and of subsequent frames. + largestQueuedTimestampUs = Long.MIN_VALUE; + for (int i = queueSize - 1; i >= 0; i--) { + int sampleIndex = (relativeReadIndex + i) % capacity; + largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timesUs[sampleIndex]); + if ((flags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + break; + } + } + return offsets[relativeWriteIndex]; + } + + public void sourceId(int sourceId) { + upstreamSourceId = sourceId; + } + + // Called by the consuming thread. + + /** + * Returns the current absolute read index. + */ + public int getReadIndex() { + return absoluteReadIndex; + } + + /** + * Peeks the source id of the next sample, or the current upstream source id if the queue is + * empty. + */ + public int peekSourceId() { + return queueSize == 0 ? upstreamSourceId : sourceIds[relativeReadIndex]; + } + + /** + * Returns whether the queue is empty. + */ + public synchronized boolean isEmpty() { + return queueSize == 0; + } + + /** + * Returns the upstream {@link Format} in which samples are being queued. + */ + public synchronized Format getUpstreamFormat() { + return upstreamFormatRequired ? null : upstreamFormat; + } + + /** + * Returns the largest sample timestamp that has been queued since the last call to + * {@link #resetLargestParsedTimestamps()}. + *

      + * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not + * considered as having been queued. Samples that were dequeued from the front of the queue are + * considered as having been queued. + * + * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no + * samples have been queued. + */ + public synchronized long getLargestQueuedTimestampUs() { + return Math.max(largestDequeuedTimestampUs, largestQueuedTimestampUs); + } + + /** + * Attempts to read from the queue. + * + * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. + * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the + * end of the stream. If a sample is read then the buffer is populated with information + * about the sample, but not its data. The size and absolute position of the data in the + * rolling buffer is stored in {@code extrasHolder}, along with an encryption id if present + * and the absolute position of the first byte that may still be required after the current + * sample has been read. May be null if the caller requires that the format of the stream be + * read even if it's not changing. + * @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 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. + * @param extrasHolder The holder into which extra sample information should be written. + * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} + * or {@link C#RESULT_BUFFER_READ}. + */ + @SuppressWarnings("ReferenceEquality") + public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired, boolean loadingFinished, Format downstreamFormat, + SampleExtrasHolder extrasHolder) { + if (queueSize == 0) { + if (loadingFinished) { + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } else if (upstreamFormat != null + && (formatRequired || upstreamFormat != downstreamFormat)) { + formatHolder.format = upstreamFormat; + return C.RESULT_FORMAT_READ; + } else { + return C.RESULT_NOTHING_READ; + } + } + + if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { + formatHolder.format = formats[relativeReadIndex]; + return C.RESULT_FORMAT_READ; + } + + if (buffer.isFlagsOnly()) { + return C.RESULT_NOTHING_READ; + } + + buffer.timeUs = timesUs[relativeReadIndex]; + buffer.setFlags(flags[relativeReadIndex]); + extrasHolder.size = sizes[relativeReadIndex]; + extrasHolder.offset = offsets[relativeReadIndex]; + extrasHolder.cryptoData = cryptoDatas[relativeReadIndex]; + + largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, buffer.timeUs); + queueSize--; + relativeReadIndex++; + absoluteReadIndex++; + if (relativeReadIndex == capacity) { + // Wrap around. + relativeReadIndex = 0; + } + + extrasHolder.nextOffset = queueSize > 0 ? offsets[relativeReadIndex] + : extrasHolder.offset + extrasHolder.size; + return C.RESULT_BUFFER_READ; + } + + /** + * Skips all samples in the buffer. + * + * @return The offset up to which data should be dropped, or {@link C#POSITION_UNSET} if no + * dropping of data is required. + */ + public synchronized long skipAll() { + if (queueSize == 0) { + return C.POSITION_UNSET; + } + + int lastSampleIndex = (relativeReadIndex + queueSize - 1) % capacity; + relativeReadIndex = (relativeReadIndex + queueSize) % capacity; + absoluteReadIndex += queueSize; + queueSize = 0; + return offsets[lastSampleIndex] + sizes[lastSampleIndex]; + } + + /** + * Attempts to locate the keyframe before or at the specified time. If + * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs} + * falls within the buffer. + * + * @param timeUs The seek time. + * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end + * of the buffer. + * @return The offset of the keyframe's data if the keyframe was present. + * {@link C#POSITION_UNSET} otherwise. + */ + public synchronized long skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) { + if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) { + return C.POSITION_UNSET; + } + + if (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer) { + return C.POSITION_UNSET; + } + + // This could be optimized to use a binary search, however in practice callers to this method + // often pass times near to the start of the buffer. Hence it's unclear whether switching to + // a binary search would yield any real benefit. + int sampleCount = 0; + int sampleCountToKeyframe = -1; + int searchIndex = relativeReadIndex; + while (searchIndex != relativeWriteIndex) { + if (timesUs[searchIndex] > timeUs) { + // We've gone too far. + break; + } else if ((flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + // We've found a keyframe, and we're still before the seek position. + sampleCountToKeyframe = sampleCount; + } + searchIndex = (searchIndex + 1) % capacity; + sampleCount++; + } + + if (sampleCountToKeyframe == -1) { + return C.POSITION_UNSET; + } + + relativeReadIndex = (relativeReadIndex + sampleCountToKeyframe) % capacity; + absoluteReadIndex += sampleCountToKeyframe; + queueSize -= sampleCountToKeyframe; + return offsets[relativeReadIndex]; + } + + // Called by the loading thread. + + public synchronized boolean format(Format format) { + if (format == null) { + upstreamFormatRequired = true; + return false; + } + 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; + return true; + } + } + + public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset, + int size, CryptoData cryptoData) { + if (upstreamKeyframeRequired) { + if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) { + return; + } + upstreamKeyframeRequired = false; + } + Assertions.checkState(!upstreamFormatRequired); + commitSampleTimestamp(timeUs); + timesUs[relativeWriteIndex] = timeUs; + offsets[relativeWriteIndex] = offset; + sizes[relativeWriteIndex] = size; + flags[relativeWriteIndex] = sampleFlags; + cryptoDatas[relativeWriteIndex] = cryptoData; + formats[relativeWriteIndex] = upstreamFormat; + sourceIds[relativeWriteIndex] = upstreamSourceId; + // Increment the write index. + queueSize++; + if (queueSize == capacity) { + // Increase the capacity. + int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT; + int[] newSourceIds = new int[newCapacity]; + long[] newOffsets = new long[newCapacity]; + long[] newTimesUs = new long[newCapacity]; + int[] newFlags = new int[newCapacity]; + int[] newSizes = new int[newCapacity]; + CryptoData[] newCryptoDatas = new CryptoData[newCapacity]; + Format[] newFormats = new Format[newCapacity]; + int beforeWrap = capacity - relativeReadIndex; + System.arraycopy(offsets, relativeReadIndex, newOffsets, 0, beforeWrap); + System.arraycopy(timesUs, relativeReadIndex, newTimesUs, 0, beforeWrap); + System.arraycopy(flags, relativeReadIndex, newFlags, 0, beforeWrap); + System.arraycopy(sizes, relativeReadIndex, newSizes, 0, beforeWrap); + System.arraycopy(cryptoDatas, relativeReadIndex, newCryptoDatas, 0, beforeWrap); + System.arraycopy(formats, relativeReadIndex, newFormats, 0, beforeWrap); + System.arraycopy(sourceIds, relativeReadIndex, newSourceIds, 0, beforeWrap); + int afterWrap = relativeReadIndex; + System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap); + System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap); + System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap); + System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap); + System.arraycopy(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap); + System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap); + System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap); + offsets = newOffsets; + timesUs = newTimesUs; + flags = newFlags; + sizes = newSizes; + cryptoDatas = newCryptoDatas; + formats = newFormats; + sourceIds = newSourceIds; + relativeReadIndex = 0; + relativeWriteIndex = capacity; + queueSize = capacity; + capacity = newCapacity; + } else { + relativeWriteIndex++; + if (relativeWriteIndex == capacity) { + // Wrap around. + relativeWriteIndex = 0; + } + } + } + + public synchronized void commitSampleTimestamp(long timeUs) { + largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); + } + + /** + * Attempts to discard samples from the tail of the queue to allow samples starting from the + * specified timestamp to be spliced in. + * + * @param timeUs The timestamp at which the splice occurs. + * @return Whether the splice was successful. + */ + public synchronized boolean attemptSplice(long timeUs) { + if (largestDequeuedTimestampUs >= timeUs) { + return false; + } + int retainCount = queueSize; + while (retainCount > 0 + && timesUs[(relativeReadIndex + retainCount - 1) % capacity] >= timeUs) { + retainCount--; + } + discardUpstreamSamples(absoluteReadIndex + retainCount); + return true; + } + +} From 047e0eb645826b4ba5cd0502319c238ba2550808 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 8 Jun 2017 10:18:45 -0700 Subject: [PATCH 102/220] Renames to prepare for upcoming media buffer changes Currently, media is discarded from DefaultTrackOutput and SampleMetadataQueue as soon as it's been read. In upcoming changes we'll decouple discard and read. This will make it possible to retain already-read media in these buffer classes, and allow the read position to be moved backward as far as media is retained. This is important for fixing an edge case around 608/EMSG tracks, and could also underpin future features like allowing retaining of X-seconds past media in the buffer. This change renames some variables and methods to prepare for the upcoming changes. read/write indices are renamed to start/end. The upcoming changes will add a read index that's between the two. isEmpty is inverted and renamed to hasNextSample, since it will be possible to not have a next sample (because the read index == end index) but for the buffer to not be empty (because start index < read index). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158409630 --- .../extractor/DefaultTrackOutput.java | 6 +- .../extractor/SampleMetadataQueue.java | 156 +++++++++--------- .../source/ExtractorMediaPeriod.java | 2 +- .../source/chunk/ChunkSampleStream.java | 4 +- .../source/hls/HlsSampleStreamWrapper.java | 2 +- 5 files changed, 85 insertions(+), 85 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java index 09970aaff0..d627f3fe3c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java @@ -180,10 +180,10 @@ public final class DefaultTrackOutput implements TrackOutput { } /** - * Returns whether the buffer is empty. + * Returns whether a sample is available to be read. */ - public boolean isEmpty() { - return metadataQueue.isEmpty(); + public boolean hasNextSample() { + return metadataQueue.hasNextSample(); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SampleMetadataQueue.java index 9802392913..452b540e53 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SampleMetadataQueue.java @@ -51,10 +51,10 @@ import com.google.android.exoplayer2.util.Util; private CryptoData[] cryptoDatas; private Format[] formats; - private int queueSize; - private int absoluteReadIndex; - private int relativeReadIndex; - private int relativeWriteIndex; + private int length; + private int absoluteStartIndex; + private int relativeStartIndex; + private int relativeEndIndex; private long largestDequeuedTimestampUs; private long largestQueuedTimestampUs; @@ -79,10 +79,10 @@ import com.google.android.exoplayer2.util.Util; } public void clearSampleData() { - absoluteReadIndex = 0; - relativeReadIndex = 0; - relativeWriteIndex = 0; - queueSize = 0; + absoluteStartIndex = 0; + relativeStartIndex = 0; + relativeEndIndex = 0; + length = 0; upstreamKeyframeRequired = true; } @@ -97,7 +97,7 @@ import com.google.android.exoplayer2.util.Util; * Returns the current absolute write index. */ public int getWriteIndex() { - return absoluteReadIndex + queueSize; + return absoluteStartIndex + length; } /** @@ -108,30 +108,30 @@ import com.google.android.exoplayer2.util.Util; */ public long discardUpstreamSamples(int discardFromIndex) { int discardCount = getWriteIndex() - discardFromIndex; - Assertions.checkArgument(0 <= discardCount && discardCount <= queueSize); + Assertions.checkArgument(0 <= discardCount && discardCount <= length); if (discardCount == 0) { - if (absoluteReadIndex == 0) { - // queueSize == absoluteReadIndex == 0, so nothing has been written to the queue. + if (absoluteStartIndex == 0) { + // length == absoluteStartIndex == 0, so nothing has been written to the queue. return 0; } - int lastWriteIndex = (relativeWriteIndex == 0 ? capacity : relativeWriteIndex) - 1; + int lastWriteIndex = (relativeEndIndex == 0 ? capacity : relativeEndIndex) - 1; return offsets[lastWriteIndex] + sizes[lastWriteIndex]; } - queueSize -= discardCount; - relativeWriteIndex = (relativeWriteIndex + capacity - discardCount) % capacity; + length -= discardCount; + relativeEndIndex = (relativeEndIndex + capacity - discardCount) % capacity; // Update the largest queued timestamp, assuming that the timestamps prior to a keyframe are // always less than the timestamp of the keyframe itself, and of subsequent frames. largestQueuedTimestampUs = Long.MIN_VALUE; - for (int i = queueSize - 1; i >= 0; i--) { - int sampleIndex = (relativeReadIndex + i) % capacity; + for (int i = length - 1; i >= 0; i--) { + int sampleIndex = (relativeStartIndex + i) % capacity; largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timesUs[sampleIndex]); if ((flags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { break; } } - return offsets[relativeWriteIndex]; + return offsets[relativeEndIndex]; } public void sourceId(int sourceId) { @@ -144,22 +144,22 @@ import com.google.android.exoplayer2.util.Util; * Returns the current absolute read index. */ public int getReadIndex() { - return absoluteReadIndex; + return absoluteStartIndex; } /** - * Peeks the source id of the next sample, or the current upstream source id if the queue is - * empty. + * Peeks the source id of the next sample, or the current upstream source id if + * {@link #hasNextSample()} is {@code false}. */ public int peekSourceId() { - return queueSize == 0 ? upstreamSourceId : sourceIds[relativeReadIndex]; + return hasNextSample() ? sourceIds[relativeStartIndex] : upstreamSourceId; } /** - * Returns whether the queue is empty. + * Returns whether a sample is available to be read. */ - public synchronized boolean isEmpty() { - return queueSize == 0; + public synchronized boolean hasNextSample() { + return length != 0; } /** @@ -209,7 +209,7 @@ import com.google.android.exoplayer2.util.Util; public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, boolean loadingFinished, Format downstreamFormat, SampleExtrasHolder extrasHolder) { - if (queueSize == 0) { + if (!hasNextSample()) { if (loadingFinished) { buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); return C.RESULT_BUFFER_READ; @@ -222,8 +222,8 @@ import com.google.android.exoplayer2.util.Util; } } - if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { - formatHolder.format = formats[relativeReadIndex]; + if (formatRequired || formats[relativeStartIndex] != downstreamFormat) { + formatHolder.format = formats[relativeStartIndex]; return C.RESULT_FORMAT_READ; } @@ -231,22 +231,22 @@ import com.google.android.exoplayer2.util.Util; return C.RESULT_NOTHING_READ; } - buffer.timeUs = timesUs[relativeReadIndex]; - buffer.setFlags(flags[relativeReadIndex]); - extrasHolder.size = sizes[relativeReadIndex]; - extrasHolder.offset = offsets[relativeReadIndex]; - extrasHolder.cryptoData = cryptoDatas[relativeReadIndex]; + buffer.timeUs = timesUs[relativeStartIndex]; + buffer.setFlags(flags[relativeStartIndex]); + extrasHolder.size = sizes[relativeStartIndex]; + extrasHolder.offset = offsets[relativeStartIndex]; + extrasHolder.cryptoData = cryptoDatas[relativeStartIndex]; largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, buffer.timeUs); - queueSize--; - relativeReadIndex++; - absoluteReadIndex++; - if (relativeReadIndex == capacity) { + length--; + relativeStartIndex++; + absoluteStartIndex++; + if (relativeStartIndex == capacity) { // Wrap around. - relativeReadIndex = 0; + relativeStartIndex = 0; } - extrasHolder.nextOffset = queueSize > 0 ? offsets[relativeReadIndex] + extrasHolder.nextOffset = length > 0 ? offsets[relativeStartIndex] : extrasHolder.offset + extrasHolder.size; return C.RESULT_BUFFER_READ; } @@ -258,14 +258,14 @@ import com.google.android.exoplayer2.util.Util; * dropping of data is required. */ public synchronized long skipAll() { - if (queueSize == 0) { + if (!hasNextSample()) { return C.POSITION_UNSET; } - int lastSampleIndex = (relativeReadIndex + queueSize - 1) % capacity; - relativeReadIndex = (relativeReadIndex + queueSize) % capacity; - absoluteReadIndex += queueSize; - queueSize = 0; + int lastSampleIndex = (relativeStartIndex + length - 1) % capacity; + relativeStartIndex = (relativeStartIndex + length) % capacity; + absoluteStartIndex += length; + length = 0; return offsets[lastSampleIndex] + sizes[lastSampleIndex]; } @@ -281,7 +281,7 @@ import com.google.android.exoplayer2.util.Util; * {@link C#POSITION_UNSET} otherwise. */ public synchronized long skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) { - if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) { + if (!hasNextSample() || timeUs < timesUs[relativeStartIndex]) { return C.POSITION_UNSET; } @@ -294,8 +294,8 @@ import com.google.android.exoplayer2.util.Util; // a binary search would yield any real benefit. int sampleCount = 0; int sampleCountToKeyframe = -1; - int searchIndex = relativeReadIndex; - while (searchIndex != relativeWriteIndex) { + int searchIndex = relativeStartIndex; + while (searchIndex != relativeEndIndex) { if (timesUs[searchIndex] > timeUs) { // We've gone too far. break; @@ -311,10 +311,10 @@ import com.google.android.exoplayer2.util.Util; return C.POSITION_UNSET; } - relativeReadIndex = (relativeReadIndex + sampleCountToKeyframe) % capacity; - absoluteReadIndex += sampleCountToKeyframe; - queueSize -= sampleCountToKeyframe; - return offsets[relativeReadIndex]; + relativeStartIndex = (relativeStartIndex + sampleCountToKeyframe) % capacity; + absoluteStartIndex += sampleCountToKeyframe; + length -= sampleCountToKeyframe; + return offsets[relativeStartIndex]; } // Called by the loading thread. @@ -344,16 +344,16 @@ import com.google.android.exoplayer2.util.Util; } Assertions.checkState(!upstreamFormatRequired); commitSampleTimestamp(timeUs); - timesUs[relativeWriteIndex] = timeUs; - offsets[relativeWriteIndex] = offset; - sizes[relativeWriteIndex] = size; - flags[relativeWriteIndex] = sampleFlags; - cryptoDatas[relativeWriteIndex] = cryptoData; - formats[relativeWriteIndex] = upstreamFormat; - sourceIds[relativeWriteIndex] = upstreamSourceId; + timesUs[relativeEndIndex] = timeUs; + offsets[relativeEndIndex] = offset; + sizes[relativeEndIndex] = size; + flags[relativeEndIndex] = sampleFlags; + cryptoDatas[relativeEndIndex] = cryptoData; + formats[relativeEndIndex] = upstreamFormat; + sourceIds[relativeEndIndex] = upstreamSourceId; // Increment the write index. - queueSize++; - if (queueSize == capacity) { + length++; + if (length == capacity) { // Increase the capacity. int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT; int[] newSourceIds = new int[newCapacity]; @@ -363,15 +363,15 @@ import com.google.android.exoplayer2.util.Util; int[] newSizes = new int[newCapacity]; CryptoData[] newCryptoDatas = new CryptoData[newCapacity]; Format[] newFormats = new Format[newCapacity]; - int beforeWrap = capacity - relativeReadIndex; - System.arraycopy(offsets, relativeReadIndex, newOffsets, 0, beforeWrap); - System.arraycopy(timesUs, relativeReadIndex, newTimesUs, 0, beforeWrap); - System.arraycopy(flags, relativeReadIndex, newFlags, 0, beforeWrap); - System.arraycopy(sizes, relativeReadIndex, newSizes, 0, beforeWrap); - System.arraycopy(cryptoDatas, relativeReadIndex, newCryptoDatas, 0, beforeWrap); - System.arraycopy(formats, relativeReadIndex, newFormats, 0, beforeWrap); - System.arraycopy(sourceIds, relativeReadIndex, newSourceIds, 0, beforeWrap); - int afterWrap = relativeReadIndex; + int beforeWrap = capacity - relativeStartIndex; + System.arraycopy(offsets, relativeStartIndex, newOffsets, 0, beforeWrap); + System.arraycopy(timesUs, relativeStartIndex, newTimesUs, 0, beforeWrap); + System.arraycopy(flags, relativeStartIndex, newFlags, 0, beforeWrap); + System.arraycopy(sizes, relativeStartIndex, newSizes, 0, beforeWrap); + System.arraycopy(cryptoDatas, relativeStartIndex, newCryptoDatas, 0, beforeWrap); + System.arraycopy(formats, relativeStartIndex, newFormats, 0, beforeWrap); + System.arraycopy(sourceIds, relativeStartIndex, newSourceIds, 0, beforeWrap); + int afterWrap = relativeStartIndex; System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap); System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap); System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap); @@ -386,15 +386,15 @@ import com.google.android.exoplayer2.util.Util; cryptoDatas = newCryptoDatas; formats = newFormats; sourceIds = newSourceIds; - relativeReadIndex = 0; - relativeWriteIndex = capacity; - queueSize = capacity; + relativeStartIndex = 0; + relativeEndIndex = capacity; + length = capacity; capacity = newCapacity; } else { - relativeWriteIndex++; - if (relativeWriteIndex == capacity) { + relativeEndIndex++; + if (relativeEndIndex == capacity) { // Wrap around. - relativeWriteIndex = 0; + relativeEndIndex = 0; } } } @@ -414,12 +414,12 @@ import com.google.android.exoplayer2.util.Util; if (largestDequeuedTimestampUs >= timeUs) { return false; } - int retainCount = queueSize; + int retainCount = length; while (retainCount > 0 - && timesUs[(relativeReadIndex + retainCount - 1) % capacity] >= timeUs) { + && timesUs[(relativeStartIndex + retainCount - 1) % capacity] >= timeUs) { retainCount--; } - discardUpstreamSamples(absoluteReadIndex + retainCount); + discardUpstreamSamples(absoluteStartIndex + retainCount); return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index f247d4dd37..7a6424635f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -323,7 +323,7 @@ import java.io.IOException; // SampleStream methods. /* package */ boolean isReady(int track) { - return loadingFinished || (!isPendingReset() && !sampleQueues.valueAt(track).isEmpty()); + return loadingFinished || (!isPendingReset() && sampleQueues.valueAt(track).hasNextSample()); } /* package */ void maybeThrowError() throws IOException { 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 8f32eb46b8..b4399f0f81 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 @@ -228,7 +228,7 @@ public class ChunkSampleStream implements SampleStream, S @Override public boolean isReady() { - return loadingFinished || (!isPendingReset() && !primarySampleQueue.isEmpty()); + return loadingFinished || (!isPendingReset() && primarySampleQueue.hasNextSample()); } @Override @@ -451,7 +451,7 @@ public class ChunkSampleStream implements SampleStream, S @Override public boolean isReady() { - return loadingFinished || (!isPendingReset() && !sampleQueue.isEmpty()); + return loadingFinished || (!isPendingReset() && sampleQueue.hasNextSample()); } @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 367c43caf1..a73553263b 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 @@ -282,7 +282,7 @@ import java.util.LinkedList; // SampleStream implementation. /* package */ boolean isReady(int group) { - return loadingFinished || (!isPendingReset() && !sampleQueues.valueAt(group).isEmpty()); + return loadingFinished || (!isPendingReset() && sampleQueues.valueAt(group).hasNextSample()); } /* package */ void maybeThrowError() throws IOException { From 4e006a9616427a1b45ffd13d09a93b653fa6472c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 9 Jun 2017 01:52:54 -0700 Subject: [PATCH 103/220] Move positionUs parameter from createPeriod to prepare ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158494794 --- .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 24 +++++++++---------- .../android/exoplayer2/ExoPlayerTest.java | 6 ++--- .../android/exoplayer2/TimelineTest.java | 2 +- .../exoplayer2/ExoPlayerImplInternal.java | 5 ++-- .../source/ClippingMediaPeriod.java | 4 ++-- .../source/ClippingMediaSource.java | 6 ++--- .../source/ConcatenatingMediaSource.java | 6 ++--- .../source/ExtractorMediaPeriod.java | 2 +- .../source/ExtractorMediaSource.java | 2 +- .../exoplayer2/source/LoopingMediaSource.java | 6 ++--- .../exoplayer2/source/MediaPeriod.java | 10 ++++---- .../exoplayer2/source/MediaSource.java | 3 +-- .../exoplayer2/source/MergingMediaPeriod.java | 4 ++-- .../exoplayer2/source/MergingMediaSource.java | 4 ++-- .../source/SingleSampleMediaPeriod.java | 2 +- .../source/SingleSampleMediaSource.java | 2 +- .../source/dash/DashMediaPeriod.java | 2 +- .../source/dash/DashMediaSource.java | 2 +- .../exoplayer2/source/hls/HlsMediaPeriod.java | 23 ++++++++---------- .../exoplayer2/source/hls/HlsMediaSource.java | 4 ++-- .../source/smoothstreaming/SsMediaPeriod.java | 2 +- .../source/smoothstreaming/SsMediaSource.java | 2 +- 22 files changed, 59 insertions(+), 64 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 0055fbca32..8e8c5aca19 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -159,12 +159,12 @@ public final class ImaAdsMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + public MediaPeriod createPeriod(int index, Allocator allocator) { if (timeline.isPeriodAd(index)) { int adBreakIndex = timeline.getAdBreakIndex(index); int adIndexInAdBreak = timeline.getAdIndexInAdBreak(index); if (adIndexInAdBreak >= adBreakMediaSources[adBreakIndex].length) { - DeferredMediaPeriod deferredPeriod = new DeferredMediaPeriod(0, allocator, positionUs); + DeferredMediaPeriod deferredPeriod = new DeferredMediaPeriod(0, allocator); if (adIndexInAdBreak >= adBreakDeferredMediaPeriods[adBreakIndex].length) { adBreakDeferredMediaPeriods[adBreakIndex] = Arrays.copyOf( adBreakDeferredMediaPeriods[adBreakIndex], adIndexInAdBreak + 1); @@ -174,15 +174,13 @@ public final class ImaAdsMediaSource implements MediaSource { } MediaSource adBreakMediaSource = adBreakMediaSources[adBreakIndex][adIndexInAdBreak]; - MediaPeriod adBreakMediaPeriod = adBreakMediaSource.createPeriod(0, allocator, positionUs); + MediaPeriod adBreakMediaPeriod = adBreakMediaSource.createPeriod(0, allocator); mediaSourceByMediaPeriod.put(adBreakMediaPeriod, adBreakMediaSource); return adBreakMediaPeriod; } else { long startUs = timeline.getContentStartTimeUs(index); long endUs = timeline.getContentEndTimeUs(index); - long contentStartUs = startUs + positionUs; - MediaPeriod contentMediaPeriod = contentMediaSource.createPeriod(0, allocator, - contentStartUs); + MediaPeriod contentMediaPeriod = contentMediaSource.createPeriod(0, allocator); ClippingMediaPeriod clippingPeriod = new ClippingMediaPeriod(contentMediaPeriod); clippingPeriod.setClipping(startUs, endUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE : endUs); mediaSourceByMediaPeriod.put(contentMediaPeriod, contentMediaSource); @@ -436,29 +434,29 @@ public final class ImaAdsMediaSource implements MediaSource { private final int index; private final Allocator allocator; - private final long positionUs; public MediaPeriod mediaPeriod; private MediaPeriod.Callback callback; + private long positionUs; - public DeferredMediaPeriod(int index, Allocator allocator, long positionUs) { + public DeferredMediaPeriod(int index, Allocator allocator) { this.index = index; this.allocator = allocator; - this.positionUs = positionUs; } public void setMediaSource(MediaSource mediaSource) { - mediaPeriod = mediaSource.createPeriod(index, allocator, positionUs); + mediaPeriod = mediaSource.createPeriod(index, allocator); if (callback != null) { - mediaPeriod.prepare(this); + mediaPeriod.prepare(this, positionUs); } } @Override - public void prepare(Callback callback) { + public void prepare(Callback callback, long positionUs) { this.callback = callback; + this.positionUs = positionUs; if (mediaPeriod != null) { - mediaPeriod.prepare(this); + mediaPeriod.prepare(this, positionUs); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 2d4ff98947..f8217ebf11 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -535,11 +535,10 @@ public final class ExoPlayerTest extends TestCase { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + public MediaPeriod createPeriod(int index, Allocator allocator) { Assertions.checkIndex(index, 0, timeline.getPeriodCount()); assertTrue(preparedSource); assertFalse(releasedSource); - assertEquals(0, positionUs); FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); activeMediaPeriods.add(mediaPeriod); return mediaPeriod; @@ -583,8 +582,9 @@ public final class ExoPlayerTest extends TestCase { } @Override - public void prepare(Callback callback) { + public void prepare(Callback callback, long positionUs) { assertFalse(preparedPeriod); + assertEquals(0, positionUs); preparedPeriod = true; callback.onPrepared(this); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java index fc3ccacbf2..807297910d 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java @@ -87,7 +87,7 @@ public class TimelineTest extends TestCase { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + public MediaPeriod createPeriod(int index, Allocator allocator) { return null; } 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 0f0b18c7b4..be0e6a432e 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 @@ -1352,7 +1352,7 @@ import java.io.IOException; loadingPeriodHolder.next = newPeriodHolder; } loadingPeriodHolder = newPeriodHolder; - loadingPeriodHolder.mediaPeriod.prepare(this); + loadingPeriodHolder.mediaPeriod.prepare(this, newLoadingPeriodStartPositionUs); setIsLoading(true); } @@ -1528,8 +1528,7 @@ import java.io.IOException; this.startPositionUs = startPositionUs; sampleStreams = new SampleStream[renderers.length]; mayRetainStreamFlags = new boolean[renderers.length]; - mediaPeriod = mediaSource.createPeriod(periodIndex, loadControl.getAllocator(), - startPositionUs); + mediaPeriod = mediaSource.createPeriod(periodIndex, loadControl.getAllocator()); } public long toRendererTime(long periodTimeUs) { 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 e14930c7b8..a7690d8d74 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 @@ -68,9 +68,9 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } @Override - public void prepare(MediaPeriod.Callback callback) { + public void prepare(MediaPeriod.Callback callback, long positionUs) { this.callback = callback; - mediaPeriod.prepare(this); + mediaPeriod.prepare(this, startUs + positionUs); } @Override 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 c61dea9553..c1ae082203 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 @@ -69,9 +69,9 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod( - mediaSource.createPeriod(index, allocator, startUs + positionUs)); + public MediaPeriod createPeriod(int index, Allocator allocator) { + ClippingMediaPeriod mediaPeriod = + new ClippingMediaPeriod(mediaSource.createPeriod(index, allocator)); mediaPeriods.add(mediaPeriod); mediaPeriod.setClipping(clippingTimeline.startUs, clippingTimeline.endUs); return mediaPeriod; 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 2299e757d7..6263800e05 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 @@ -91,11 +91,11 @@ public final class ConcatenatingMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + public MediaPeriod createPeriod(int index, Allocator allocator) { int sourceIndex = timeline.getChildIndexForPeriod(index); int periodIndexInSource = index - timeline.getFirstPeriodIndexInChild(sourceIndex); - MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIndexInSource, allocator, - positionUs); + MediaPeriod mediaPeriod = + mediaSources[sourceIndex].createPeriod(periodIndexInSource, allocator); sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex); return mediaPeriod; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 7a6424635f..5c86ead25c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -159,7 +159,7 @@ import java.io.IOException; } @Override - public void prepare(Callback callback) { + public void prepare(Callback callback, long positionUs) { this.callback = callback; loadCondition.open(); startLoading(); 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 c560616aae..618f579a94 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 @@ -156,7 +156,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + public MediaPeriod createPeriod(int index, Allocator allocator) { Assertions.checkArgument(index == 0); return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(), extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener, 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 a97f7ecd95..240a2b9350 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 @@ -76,10 +76,10 @@ public final class LoopingMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + public MediaPeriod createPeriod(int index, Allocator allocator) { return loopCount != Integer.MAX_VALUE - ? childSource.createPeriod(index % childPeriodCount, allocator, positionUs) - : childSource.createPeriod(index, allocator, positionUs); + ? childSource.createPeriod(index % childPeriodCount, allocator) + : childSource.createPeriod(index, allocator); } @Override 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 3b06542855..90d72dd907 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 @@ -55,8 +55,10 @@ public interface MediaPeriod extends SequenceableLoader { * * @param callback Callback to receive updates from this period, including being notified when * preparation completes. + * @param positionUs The position in microseconds relative to the start of the period at which to + * start loading data. */ - void prepare(Callback callback); + void prepare(Callback callback, long positionUs); /** * Throws an error that's preventing the period from becoming prepared. Does nothing if no such @@ -162,9 +164,9 @@ public interface MediaPeriod extends SequenceableLoader { * This method may be called both during and after the period has been prepared. *

      * A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the - * {@link Callback} passed to {@link #prepare(Callback)} to request that this method be called - * when the period is permitted to continue loading data. A period may do this both during and - * after preparation. + * {@link Callback} passed to {@link #prepare(Callback, long)} to request that this method be + * called when the period is permitted to continue loading data. A period may do this both during + * and after preparation. * * @param positionUs The current playback position. * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return 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 f013e790f7..08c238fca7 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 @@ -64,10 +64,9 @@ public interface MediaSource { * * @param index The index of the period. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. - * @param positionUs The player's current playback position. * @return A new {@link MediaPeriod}. */ - MediaPeriod createPeriod(int index, Allocator allocator, long positionUs); + MediaPeriod createPeriod(int index, Allocator allocator); /** * Releases the period. 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 077b5576c1..cfb75b1b87 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 @@ -44,11 +44,11 @@ import java.util.IdentityHashMap; } @Override - public void prepare(Callback callback) { + public void prepare(Callback callback, long positionUs) { this.callback = callback; pendingChildPrepareCount = periods.length; for (MediaPeriod period : periods) { - period.prepare(this); + period.prepare(this, positionUs); } } 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 6f37165916..421a05adc2 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 @@ -116,10 +116,10 @@ public final class MergingMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + public MediaPeriod createPeriod(int index, Allocator allocator) { MediaPeriod[] periods = new MediaPeriod[mediaSources.length]; for (int i = 0; i < periods.length; i++) { - periods[i] = mediaSources[i].createPeriod(index, allocator, positionUs); + periods[i] = mediaSources[i].createPeriod(index, allocator); } return new MergingMediaPeriod(periods); } 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 8e38588e89..3435c01eeb 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 @@ -79,7 +79,7 @@ import java.util.Arrays; } @Override - public void prepare(Callback callback) { + public void prepare(Callback callback, long positionUs) { callback.onPrepared(this); } 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 f6ee84a6f4..7544176c54 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 @@ -95,7 +95,7 @@ public final class SingleSampleMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + public MediaPeriod createPeriod(int index, Allocator allocator) { Assertions.checkArgument(index == 0); return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount, eventHandler, eventListener, eventSourceId); 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 6b9668e4b9..905c82364a 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 @@ -106,7 +106,7 @@ import java.util.List; } @Override - public void prepare(Callback callback) { + public void prepare(Callback callback, long positionUs) { this.callback = callback; callback.onPrepared(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 111729d361..82503673e5 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 @@ -281,7 +281,7 @@ public final class DashMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int periodIndex, Allocator allocator, long positionUs) { + public MediaPeriod createPeriod(int periodIndex, Allocator allocator) { EventDispatcher periodEventDispatcher = eventDispatcher.copyWithMediaTimeOffsetMs( manifest.getPeriod(periodIndex).startMs); DashMediaPeriod mediaPeriod = new DashMediaPeriod(firstPeriodId + periodIndex, manifest, 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 3a833f5468..25e48d8cce 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 @@ -51,7 +51,6 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final IdentityHashMap streamWrapperIndices; private final TimestampAdjusterProvider timestampAdjusterProvider; private final Handler continueLoadingHandler; - private final long preparePositionUs; private Callback callback; private int pendingPrepareCount; @@ -62,8 +61,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private CompositeSequenceableLoader sequenceableLoader; public HlsMediaPeriod(HlsPlaylistTracker playlistTracker, HlsDataSourceFactory dataSourceFactory, - int minLoadableRetryCount, EventDispatcher eventDispatcher, Allocator allocator, - long positionUs) { + int minLoadableRetryCount, EventDispatcher eventDispatcher, Allocator allocator) { this.playlistTracker = playlistTracker; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; @@ -72,7 +70,6 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper streamWrapperIndices = new IdentityHashMap<>(); timestampAdjusterProvider = new TimestampAdjusterProvider(); continueLoadingHandler = new Handler(); - preparePositionUs = positionUs; } public void release() { @@ -86,10 +83,10 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } @Override - public void prepare(Callback callback) { + public void prepare(Callback callback, long positionUs) { playlistTracker.addListener(this); this.callback = callback; - buildAndPrepareSampleStreamWrappers(); + buildAndPrepareSampleStreamWrappers(positionUs); } @Override @@ -285,7 +282,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper // Internal methods. - private void buildAndPrepareSampleStreamWrappers() { + private void buildAndPrepareSampleStreamWrappers(long positionUs) { HlsMasterPlaylist masterPlaylist = playlistTracker.getMasterPlaylist(); // Build the default stream wrapper. List selectedVariants = new ArrayList<>(masterPlaylist.variants); @@ -322,7 +319,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()]; selectedVariants.toArray(variants); HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, - variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormats); + variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormats, positionUs); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.setIsTimestampMaster(true); sampleStreamWrapper.continuePreparing(); @@ -332,7 +329,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper // Build audio stream wrappers. for (int i = 0; i < audioRenditions.size(); i++) { sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, - new HlsUrl[] {audioRenditions.get(i)}, null, Collections.emptyList()); + new HlsUrl[] {audioRenditions.get(i)}, null, Collections.emptyList(), positionUs); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.continuePreparing(); } @@ -341,18 +338,18 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper for (int i = 0; i < subtitleRenditions.size(); i++) { HlsUrl url = subtitleRenditions.get(i); sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null, - Collections.emptyList()); + Collections.emptyList(), positionUs); sampleStreamWrapper.prepareSingleTrack(url.format); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; } } private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, - Format muxedAudioFormat, List muxedCaptionFormats) { + Format muxedAudioFormat, List muxedCaptionFormats, long positionUs) { HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, dataSourceFactory, timestampAdjusterProvider, muxedCaptionFormats); - return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, - preparePositionUs, muxedAudioFormat, minLoadableRetryCount, eventDispatcher); + return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, positionUs, + muxedAudioFormat, minLoadableRetryCount, eventDispatcher); } private void continuePreparingOrLoading() { 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 1bfb8371a0..5839a4af38 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 @@ -88,10 +88,10 @@ public final class HlsMediaSource implements MediaSource, } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + public MediaPeriod createPeriod(int index, Allocator allocator) { Assertions.checkArgument(index == 0); return new HlsMediaPeriod(playlistTracker, dataSourceFactory, minLoadableRetryCount, - eventDispatcher, allocator, positionUs); + eventDispatcher, allocator); } @Override 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 43cd4a9f8d..87f9c4d03b 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 @@ -93,7 +93,7 @@ import java.util.ArrayList; } @Override - public void prepare(Callback callback) { + public void prepare(Callback callback, long positionUs) { this.callback = callback; callback.onPrepared(this); } 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 d16620d5b2..855b922135 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 @@ -222,7 +222,7 @@ public final class SsMediaSource implements MediaSource, } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + public MediaPeriod createPeriod(int index, Allocator allocator) { Assertions.checkArgument(index == 0); SsMediaPeriod period = new SsMediaPeriod(manifest, chunkSourceFactory, minLoadableRetryCount, eventDispatcher, manifestLoaderErrorThrower, allocator); From 59315cf9233f2caba6a54d813458a64067c25756 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 9 Jun 2017 02:35:47 -0700 Subject: [PATCH 104/220] Fix maximum read ahead logic for repeat mode Separate MediaPeriodHolder.index and MediaPeriodHolder.periodIndex, so that the latter is always a period index (which may repeat or jump) whereas the holder index increases by one each time an item is added to the period holder queue. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158497639 --- .../exoplayer2/ExoPlayerImplInternal.java | 68 ++++++++++--------- 1 file changed, 37 insertions(+), 31 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 be0e6a432e..fd30b673be 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 @@ -433,15 +433,15 @@ import java.io.IOException; } boolean seenReadingPeriodHolder = lastValidPeriodHolder == readingPeriodHolder; boolean seenLoadingPeriodHolder = lastValidPeriodHolder == loadingPeriodHolder; - int nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.index, period, window, - repeatMode); + int nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.periodIndex, period, + window, repeatMode); while (lastValidPeriodHolder.next != null && nextPeriodIndex != C.INDEX_UNSET - && lastValidPeriodHolder.next.index == nextPeriodIndex) { + && lastValidPeriodHolder.next.periodIndex == nextPeriodIndex) { lastValidPeriodHolder = lastValidPeriodHolder.next; seenReadingPeriodHolder |= lastValidPeriodHolder == readingPeriodHolder; seenLoadingPeriodHolder |= lastValidPeriodHolder == loadingPeriodHolder; - nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.index, period, window, - repeatMode); + nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.periodIndex, period, + window, repeatMode); } // Release all period holder beyond the last one matching the new period order. if (lastValidPeriodHolder.next != null) { @@ -449,7 +449,7 @@ import java.io.IOException; lastValidPeriodHolder.next = null; } // Update isLast flag. - lastValidPeriodHolder.isLast = isLastPeriod(lastValidPeriodHolder.index); + lastValidPeriodHolder.isLast = isLastPeriod(lastValidPeriodHolder.periodIndex); // Handle cases where loadingPeriodHolder or readingPeriodHolder have been removed. if (!seenLoadingPeriodHolder) { loadingPeriodHolder = lastValidPeriodHolder; @@ -457,7 +457,7 @@ import java.io.IOException; if (!seenReadingPeriodHolder && playingPeriodHolder != null) { // Renderers may have read from a period that's been removed. Seek back to the current // position of the playing period to make sure none of the removed period is played. - int playingPeriodIndex = playingPeriodHolder.index; + int playingPeriodIndex = playingPeriodHolder.periodIndex; long newPositionUs = seekToPeriodPosition(playingPeriodIndex, playbackInfo.positionUs); playbackInfo = new PlaybackInfo(playingPeriodIndex, newPositionUs); } @@ -507,7 +507,7 @@ import java.io.IOException; long bufferedPositionUs = enabledRenderers.length == 0 ? C.TIME_END_OF_SOURCE : playingPeriodHolder.mediaPeriod.getBufferedPositionUs(); playbackInfo.bufferedPositionUs = bufferedPositionUs == C.TIME_END_OF_SOURCE - ? timeline.getPeriod(playingPeriodHolder.index, period).getDurationUs() + ? timeline.getPeriod(playingPeriodHolder.periodIndex, period).getDurationUs() : bufferedPositionUs; } @@ -560,7 +560,7 @@ import java.io.IOException; } } - long playingPeriodDurationUs = timeline.getPeriod(playingPeriodHolder.index, period) + long playingPeriodDurationUs = timeline.getPeriod(playingPeriodHolder.periodIndex, period) .getDurationUs(); if (allRenderersEnded && (playingPeriodDurationUs == C.TIME_UNSET @@ -674,7 +674,7 @@ import java.io.IOException; // Clear the timeline, but keep the requested period if it is already prepared. MediaPeriodHolder periodHolder = playingPeriodHolder; while (periodHolder != null) { - if (periodHolder.index == periodIndex && periodHolder.prepared) { + if (periodHolder.periodIndex == periodIndex && periodHolder.prepared) { newPlayingPeriodHolder = periodHolder; } else { periodHolder.release(); @@ -914,7 +914,7 @@ import java.io.IOException; if (loadingPeriodHolder.isLast) { return true; } - loadingPeriodBufferedPositionUs = timeline.getPeriod(loadingPeriodHolder.index, period) + loadingPeriodBufferedPositionUs = timeline.getPeriod(loadingPeriodHolder.periodIndex, period) .getDurationUs(); } return loadControl.shouldStartPlayback( @@ -976,7 +976,7 @@ import java.io.IOException; if (periodIndex == C.INDEX_UNSET) { // We didn't find the current period in the new timeline. Attempt to resolve a subsequent // period whose window we can restart from. - int newPeriodIndex = resolveSubsequentPeriod(periodHolder.index, oldTimeline, timeline); + int newPeriodIndex = resolveSubsequentPeriod(periodHolder.periodIndex, oldTimeline, timeline); if (newPeriodIndex == C.INDEX_UNSET) { // We failed to resolve a suitable restart position. handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); @@ -991,10 +991,11 @@ import java.io.IOException; // Clear the index of each holder that doesn't contain the default position. If a holder // contains the default position then update its index so it can be re-used when seeking. Object newPeriodUid = period.uid; - periodHolder.index = C.INDEX_UNSET; + periodHolder.periodIndex = C.INDEX_UNSET; while (periodHolder.next != null) { periodHolder = periodHolder.next; - periodHolder.index = periodHolder.uid.equals(newPeriodUid) ? newPeriodIndex : C.INDEX_UNSET; + periodHolder.periodIndex = periodHolder.uid.equals(newPeriodUid) + ? newPeriodIndex : C.INDEX_UNSET; } // Actually do the seek. newPositionUs = seekToPeriodPosition(newPeriodIndex, newPositionUs); @@ -1004,7 +1005,7 @@ import java.io.IOException; } // The current period is in the new timeline. Update the holder and playbackInfo. - periodHolder.setIndex(periodIndex, isLastPeriod(periodIndex)); + periodHolder.setPeriodIndex(periodIndex, isLastPeriod(periodIndex)); boolean seenReadingPeriod = periodHolder == readingPeriodHolder; if (periodIndex != playbackInfo.periodIndex) { playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); @@ -1019,14 +1020,14 @@ import java.io.IOException; if (periodIndex != C.INDEX_UNSET && periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) { // The holder is consistent with the new timeline. Update its index and continue. - periodHolder.setIndex(periodIndex, isLastPeriod(periodIndex)); + periodHolder.setPeriodIndex(periodIndex, isLastPeriod(periodIndex)); seenReadingPeriod |= (periodHolder == readingPeriodHolder); } else { // The holder is inconsistent with the new timeline. if (!seenReadingPeriod) { // Renderers may have read from a period that's been removed. Seek back to the current // position of the playing period to make sure none of the removed period is played. - periodIndex = playingPeriodHolder.index; + periodIndex = playingPeriodHolder.periodIndex; long newPositionUs = seekToPeriodPosition(periodIndex, playbackInfo.positionUs); playbackInfo = new PlaybackInfo(periodIndex, newPositionUs); } else { @@ -1213,7 +1214,7 @@ import java.io.IOException; // the end of the playing period, so advance playback to the next period. playingPeriodHolder.release(); setPlayingPeriodHolder(playingPeriodHolder.next); - playbackInfo = new PlaybackInfo(playingPeriodHolder.index, + playbackInfo = new PlaybackInfo(playingPeriodHolder.periodIndex, playingPeriodHolder.startPositionUs); updatePlaybackPositions(); eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); @@ -1287,17 +1288,19 @@ import java.io.IOException; if (loadingPeriodHolder == null) { newLoadingPeriodIndex = playbackInfo.periodIndex; } else { - int loadingPeriodIndex = loadingPeriodHolder.index; + int loadingPeriodIndex = loadingPeriodHolder.periodIndex; if (loadingPeriodHolder.isLast || !loadingPeriodHolder.isFullyBuffered() || timeline.getPeriod(loadingPeriodIndex, period).getDurationUs() == C.TIME_UNSET) { // Either the existing loading period is the last period, or we are not ready to advance to // loading the next period because it hasn't been fully buffered or its duration is unknown. return; } - if (playingPeriodHolder != null - && loadingPeriodIndex - playingPeriodHolder.index == MAXIMUM_BUFFER_AHEAD_PERIODS) { - // We are already buffering the maximum number of periods ahead. - return; + if (playingPeriodHolder != null) { + int bufferAheadPeriodCount = loadingPeriodHolder.index - playingPeriodHolder.index; + if (bufferAheadPeriodCount == MAXIMUM_BUFFER_AHEAD_PERIODS) { + // We are already buffering the maximum number of periods ahead. + return; + } } newLoadingPeriodIndex = timeline.getNextPeriodIndex(loadingPeriodIndex, period, window, repeatMode); @@ -1326,7 +1329,7 @@ import java.io.IOException; // interruptions). Hence we project the default start position forward by the duration of // the buffer, and start buffering from this point. long defaultPositionProjectionUs = loadingPeriodHolder.getRendererOffset() - + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() + + timeline.getPeriod(loadingPeriodHolder.periodIndex, period).getDurationUs() - rendererPositionUs; Pair defaultPosition = getPeriodPosition(timeline, newLoadingWindowIndex, C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs)); @@ -1342,12 +1345,13 @@ import java.io.IOException; long rendererPositionOffsetUs = loadingPeriodHolder == null ? newLoadingPeriodStartPositionUs + RENDERER_TIMESTAMP_OFFSET_US : (loadingPeriodHolder.getRendererOffset() - + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()); + + timeline.getPeriod(loadingPeriodHolder.periodIndex, period).getDurationUs()); + int holderIndex = loadingPeriodHolder == null ? 0 : loadingPeriodHolder.index + 1; boolean isLastPeriod = isLastPeriod(newLoadingPeriodIndex); timeline.getPeriod(newLoadingPeriodIndex, period, true); MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, period.uid, - newLoadingPeriodIndex, isLastPeriod, newLoadingPeriodStartPositionUs); + holderIndex, newLoadingPeriodIndex, isLastPeriod, newLoadingPeriodStartPositionUs); if (loadingPeriodHolder != null) { loadingPeriodHolder.next = newPeriodHolder; } @@ -1491,11 +1495,12 @@ import java.io.IOException; public final MediaPeriod mediaPeriod; public final Object uid; + public final int index; public final SampleStream[] sampleStreams; public final boolean[] mayRetainStreamFlags; public final long rendererPositionOffsetUs; - public int index; + public int periodIndex; public long startPositionUs; public boolean isLast; public boolean prepared; @@ -1514,7 +1519,7 @@ import java.io.IOException; public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities, long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl, - MediaSource mediaSource, Object periodUid, int periodIndex, boolean isLastPeriod, + MediaSource mediaSource, Object periodUid, int index, int periodIndex, boolean isLastPeriod, long startPositionUs) { this.renderers = renderers; this.rendererCapabilities = rendererCapabilities; @@ -1523,7 +1528,8 @@ import java.io.IOException; this.loadControl = loadControl; this.mediaSource = mediaSource; this.uid = Assertions.checkNotNull(periodUid); - this.index = periodIndex; + this.index = index; + this.periodIndex = periodIndex; this.isLast = isLastPeriod; this.startPositionUs = startPositionUs; sampleStreams = new SampleStream[renderers.length]; @@ -1543,8 +1549,8 @@ import java.io.IOException; return rendererPositionOffsetUs - startPositionUs; } - public void setIndex(int index, boolean isLast) { - this.index = index; + public void setPeriodIndex(int periodIndex, boolean isLast) { + this.periodIndex = periodIndex; this.isLast = isLast; } From 646047f0885524c9da4e74795a5dd76fa68854cf Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 9 Jun 2017 08:45:50 -0700 Subject: [PATCH 105/220] Add nullable annotation to onSourceInfoRefreshed's manifest argument ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158522507 --- .../com/google/android/exoplayer2/source/MediaSource.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 08c238fca7..52cb4540bd 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; @@ -34,9 +35,9 @@ public interface MediaSource { * Called when manifest and/or timeline has been refreshed. * * @param timeline The source's timeline. - * @param manifest The loaded manifest. + * @param manifest The loaded manifest. May be null. */ - void onSourceInfoRefreshed(Timeline timeline, Object manifest); + void onSourceInfoRefreshed(Timeline timeline, @Nullable Object manifest); } From cb5b6fba01ba2a60b919b0b4377e202f40e75da9 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 12 Jun 2017 00:51:19 -0700 Subject: [PATCH 106/220] Allow customization of ExtractorMediaSource's check interval ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158683582 --- .../source/ExtractorMediaPeriod.java | 14 ++++++-------- .../source/ExtractorMediaSource.java | 19 ++++++++++++++----- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 5c86ead25c..8eaa9cae5a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -64,6 +64,7 @@ import java.io.IOException; private final MediaSource.Listener sourceListener; private final Allocator allocator; private final String customCacheKey; + private final long continueLoadingCheckIntervalBytes; private final Loader loader; private final ExtractorHolder extractorHolder; private final ConditionVariable loadCondition; @@ -105,11 +106,13 @@ import java.io.IOException; * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache * indexing. May be null. + * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each + * invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}. */ public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors, int minLoadableRetryCount, Handler eventHandler, ExtractorMediaSource.EventListener eventListener, MediaSource.Listener sourceListener, - Allocator allocator, String customCacheKey) { + Allocator allocator, String customCacheKey, int continueLoadingCheckIntervalBytes) { this.uri = uri; this.dataSource = dataSource; this.minLoadableRetryCount = minLoadableRetryCount; @@ -118,6 +121,7 @@ import java.io.IOException; this.sourceListener = sourceListener; this.allocator = allocator; this.customCacheKey = customCacheKey; + this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; loader = new Loader("Loader:ExtractorMediaPeriod"); extractorHolder = new ExtractorHolder(extractors, this); loadCondition = new ConditionVariable(); @@ -585,12 +589,6 @@ import java.io.IOException; */ /* package */ final class ExtractingLoadable implements Loadable { - /** - * The number of bytes that should be loaded between each each invocation of - * {@link Callback#onContinueLoadingRequested(SequenceableLoader)}. - */ - private static final int CONTINUE_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024; - private final Uri uri; private final DataSource dataSource; private final ExtractorHolder extractorHolder; @@ -650,7 +648,7 @@ import java.io.IOException; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { loadCondition.block(); result = extractor.read(input, positionHolder); - if (input.getPosition() > position + CONTINUE_LOADING_CHECK_INTERVAL_BYTES) { + if (input.getPosition() > position + continueLoadingCheckIntervalBytes) { position = input.getPosition(); loadCondition.close(); handler.post(onContinueLoadingRequestedRunnable); 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 618f579a94..cd77146df3 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 @@ -72,6 +72,12 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List */ public static final int MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA = -1; + /** + * The default number of bytes that should be loaded between each each invocation of + * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + */ + public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024; + private final Uri uri; private final DataSource.Factory dataSourceFactory; private final ExtractorsFactory extractorsFactory; @@ -80,6 +86,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List private final EventListener eventListener; private final Timeline.Period period; private final String customCacheKey; + private final int continueLoadingCheckIntervalBytes; private MediaSource.Listener sourceListener; private Timeline timeline; @@ -96,8 +103,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List */ public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) { - this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler, - eventListener, null); + this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null); } /** @@ -115,7 +121,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener, String customCacheKey) { this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler, - eventListener, customCacheKey); + eventListener, customCacheKey, DEFAULT_LOADING_CHECK_INTERVAL_BYTES); } /** @@ -129,10 +135,12 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List * @param eventListener A listener of events. May be null if delivery of events is not required. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache * indexing. May be null. + * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each + * invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. */ public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler, - EventListener eventListener, String customCacheKey) { + EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) { this.uri = uri; this.dataSourceFactory = dataSourceFactory; this.extractorsFactory = extractorsFactory; @@ -140,6 +148,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List this.eventHandler = eventHandler; this.eventListener = eventListener; this.customCacheKey = customCacheKey; + this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; period = new Timeline.Period(); } @@ -160,7 +169,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List Assertions.checkArgument(index == 0); return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(), extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener, - this, allocator, customCacheKey); + this, allocator, customCacheKey, continueLoadingCheckIntervalBytes); } @Override From 5cd3a9baa03ca37bbc8060791596d36162d22739 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 12 Jun 2017 01:11:17 -0700 Subject: [PATCH 107/220] Fix passing of invalid surface to video renderers ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158684924 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 5 +++-- 1 file changed, 3 insertions(+), 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 8dcd390033..a8c1d1d9f0 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 @@ -221,8 +221,9 @@ public class SimpleExoPlayer implements ExoPlayer { if (surfaceHolder == null) { setVideoSurfaceInternal(null, false); } else { - setVideoSurfaceInternal(surfaceHolder.getSurface(), false); surfaceHolder.addCallback(componentListener); + Surface surface = surfaceHolder.getSurface(); + setVideoSurfaceInternal(surface != null && surface.isValid() ? surface : null, false); } } @@ -273,9 +274,9 @@ public class SimpleExoPlayer implements ExoPlayer { if (textureView.getSurfaceTextureListener() != null) { Log.w(TAG, "Replacing existing SurfaceTextureListener."); } + textureView.setSurfaceTextureListener(componentListener); SurfaceTexture surfaceTexture = textureView.getSurfaceTexture(); setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true); - textureView.setSurfaceTextureListener(componentListener); } } From c980eae9c4c9a7768cccdf574ba1fd9723750d97 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 12 Jun 2017 01:37:01 -0700 Subject: [PATCH 108/220] Allow overriding of getCodecMaxValues ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158686545 --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 6a51016dd3..7610bb1a55 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 @@ -775,7 +775,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @return Suitable {@link CodecMaxValues}. * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ - private static CodecMaxValues getCodecMaxValues(MediaCodecInfo codecInfo, Format format, + protected CodecMaxValues getCodecMaxValues(MediaCodecInfo codecInfo, Format format, Format[] streamFormats) throws DecoderQueryException { int maxWidth = format.width; int maxHeight = format.height; @@ -975,7 +975,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return format.rotationDegrees == Format.NO_VALUE ? 0 : format.rotationDegrees; } - private static final class CodecMaxValues { + protected static final class CodecMaxValues { public final int width; public final int height; From dbfbcd63125fbb34bfc98ac4786393617c3c16e1 Mon Sep 17 00:00:00 2001 From: michalliu Date: Tue, 13 Jun 2017 15:17:32 +0800 Subject: [PATCH 109/220] check if defaultRefreshRate is reasonable We found getDefaultDisplay has a very small chance returns null --- .../android/exoplayer2/video/VideoFrameReleaseTimeHelper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java index 4771f2572c..32c7325547 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java @@ -72,7 +72,7 @@ public final class VideoFrameReleaseTimeHelper { private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate, boolean useDefaultDisplayVsync) { this.useDefaultDisplayVsync = useDefaultDisplayVsync; - if (useDefaultDisplayVsync) { + if (useDefaultDisplayVsync && defaultDisplayRefreshRate > 0f) { vsyncSampler = VSyncSampler.getInstance(); vsyncDurationNs = (long) (C.NANOS_PER_SECOND / defaultDisplayRefreshRate); vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100; @@ -202,7 +202,7 @@ public final class VideoFrameReleaseTimeHelper { private static float getDefaultDisplayRefreshRate(Context context) { WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - return manager.getDefaultDisplay().getRefreshRate(); + return manager.getDefaultDisplay() != null ? manager.getDefaultDisplay().getRefreshRate() : 0f; } /** From 58280f979ee07a0fc30ed39a6e898f4966a7d901 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 12 Jun 2017 03:49:12 -0700 Subject: [PATCH 110/220] Fix continueLoading in IMA deferred periods continueLoading may be called during preparation, but this is not handled correctly in the case where a deferred period doesn't have a source yet. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158696539 --- .../google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 8e8c5aca19..00bd81d754 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -506,7 +506,7 @@ public final class ImaAdsMediaSource implements MediaSource { @Override public boolean continueLoading(long positionUs) { - return mediaPeriod.continueLoading(positionUs); + return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); } // MediaPeriod.Callback implementation. From 629edc2b9549e2151c204c09abe9e8b989fbcddf Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 12 Jun 2017 04:13:01 -0700 Subject: [PATCH 111/220] Remove needsContinueLoading from ExoPlayerImplInternal The same effect can be achieved by checking the isLoading variable of ExoPlayerImplInternal because this variable is in almost all cases set simultaneously with loadingMediaPeriodHolder.needsContinueLoading. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158697948 --- .../google/android/exoplayer2/ExoPlayerImplInternal.java | 6 +----- 1 file changed, 1 insertion(+), 5 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 fd30b673be..b93b31bdaa 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 @@ -1198,7 +1198,7 @@ import java.io.IOException; maybeUpdateLoadingPeriod(); if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { setIsLoading(false); - } else if (loadingPeriodHolder != null && loadingPeriodHolder.needsContinueLoading) { + } else if (loadingPeriodHolder != null && !isLoading) { maybeContinueLoading(); } @@ -1394,10 +1394,7 @@ import java.io.IOException; boolean continueLoading = loadControl.shouldContinueLoading(bufferedDurationUs); setIsLoading(continueLoading); if (continueLoading) { - loadingPeriodHolder.needsContinueLoading = false; loadingPeriodHolder.mediaPeriod.continueLoading(loadingPeriodPositionUs); - } else { - loadingPeriodHolder.needsContinueLoading = true; } } } @@ -1506,7 +1503,6 @@ import java.io.IOException; public boolean prepared; public boolean hasEnabledTracks; public MediaPeriodHolder next; - public boolean needsContinueLoading; public TrackSelectorResult trackSelectorResult; private final Renderer[] renderers; From dcc2f9bd67126b9ad9992941792203cde9629050 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 12 Jun 2017 07:46:48 -0700 Subject: [PATCH 112/220] Add meta data class for AbstractConcatenatedTimeline. (Preparation for GitHub issue #1706) AbstractConcatenatedTimeline repeatly calls methods of its implementation to query a specific child timeline. This may be inefficient if the implementation repeatly executes the same code to find the timeline. Changed the class such that it now queries all information at once using a meta data class. As all methods need at least two of four variables anyway, this doesn't generate unnecessary overhead. Also generified the UID for the child indices to allow new implementations to use some other UID besides the index. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158711979 --- .../source/AbstractConcatenatedTimeline.java | 142 ++++++++++-------- .../source/ConcatenatingMediaSource.java | 41 +++-- .../exoplayer2/source/LoopingMediaSource.java | 38 ++--- 3 files changed, 129 insertions(+), 92 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 37672703d7..e54dce687b 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 @@ -25,55 +25,87 @@ import com.google.android.exoplayer2.Timeline; */ /* package */ abstract class AbstractConcatenatedTimeline extends Timeline { + /** + * Meta data of a child timeline. + */ + protected static class ChildDataHolder { + + /** + * Child timeline. + */ + public Timeline timeline; + + /** + * First period index belonging to the child timeline. + */ + public int firstPeriodIndexInChild; + + /** + * First window index belonging to the child timeline. + */ + public int firstWindowIndexInChild; + + /** + * UID of child timeline. + */ + public Object uid; + + } + + private final ChildDataHolder childDataHolder; + + public AbstractConcatenatedTimeline() { + childDataHolder = new ChildDataHolder(); + } + @Override public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { - int childIndex = getChildIndexForWindow(windowIndex); - int firstWindowIndexInChild = getFirstWindowIndexInChild(childIndex); - int nextWindowIndexInChild = getChild(childIndex).getNextWindowIndex( + getChildDataByWindowIndex(windowIndex, childDataHolder); + int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild; + int nextWindowIndexInChild = childDataHolder.timeline.getNextWindowIndex( windowIndex - firstWindowIndexInChild, repeatMode == ExoPlayer.REPEAT_MODE_ALL ? ExoPlayer.REPEAT_MODE_OFF : repeatMode); - if (nextWindowIndexInChild == C.INDEX_UNSET) { - if (childIndex < getChildCount() - 1) { - childIndex++; + if (nextWindowIndexInChild != C.INDEX_UNSET) { + return firstWindowIndexInChild + nextWindowIndexInChild; + } else { + firstWindowIndexInChild += childDataHolder.timeline.getWindowCount(); + if (firstWindowIndexInChild < getWindowCount()) { + return firstWindowIndexInChild; } else if (repeatMode == ExoPlayer.REPEAT_MODE_ALL) { - childIndex = 0; + return 0; } else { return C.INDEX_UNSET; } - firstWindowIndexInChild = getFirstWindowIndexInChild(childIndex); - nextWindowIndexInChild = 0; } - return firstWindowIndexInChild + nextWindowIndexInChild; } @Override public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { - int childIndex = getChildIndexForWindow(windowIndex); - int firstWindowIndexInChild = getFirstWindowIndexInChild(childIndex); - int previousWindowIndexInChild = getChild(childIndex).getPreviousWindowIndex( + getChildDataByWindowIndex(windowIndex, childDataHolder); + int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild; + int previousWindowIndexInChild = childDataHolder.timeline.getPreviousWindowIndex( windowIndex - firstWindowIndexInChild, repeatMode == ExoPlayer.REPEAT_MODE_ALL ? ExoPlayer.REPEAT_MODE_OFF : repeatMode); - if (previousWindowIndexInChild == C.INDEX_UNSET) { - if (childIndex > 0) { - childIndex--; + if (previousWindowIndexInChild != C.INDEX_UNSET) { + return firstWindowIndexInChild + previousWindowIndexInChild; + } else { + if (firstWindowIndexInChild > 0) { + return firstWindowIndexInChild - 1; } else if (repeatMode == ExoPlayer.REPEAT_MODE_ALL) { - childIndex = getChildCount() - 1; + return getWindowCount() - 1; } else { return C.INDEX_UNSET; } - firstWindowIndexInChild = getFirstWindowIndexInChild(childIndex); - previousWindowIndexInChild = getChild(childIndex).getWindowCount() - 1; } - return firstWindowIndexInChild + previousWindowIndexInChild; } @Override public final Window getWindow(int windowIndex, Window window, boolean setIds, long defaultPositionProjectionUs) { - int childIndex = getChildIndexForWindow(windowIndex); - int firstWindowIndexInChild = getFirstWindowIndexInChild(childIndex); - int firstPeriodIndexInChild = getFirstPeriodIndexInChild(childIndex); - getChild(childIndex).getWindow(windowIndex - firstWindowIndexInChild, window, setIds, + getChildDataByWindowIndex(windowIndex, childDataHolder); + int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild; + int firstPeriodIndexInChild = childDataHolder.firstPeriodIndexInChild; + childDataHolder.timeline.getWindow(windowIndex - firstWindowIndexInChild, window, setIds, defaultPositionProjectionUs); window.firstPeriodIndex += firstPeriodIndexInChild; window.lastPeriodIndex += firstPeriodIndexInChild; @@ -82,13 +114,13 @@ import com.google.android.exoplayer2.Timeline; @Override public final Period getPeriod(int periodIndex, Period period, boolean setIds) { - int childIndex = getChildIndexForPeriod(periodIndex); - int firstWindowIndexInChild = getFirstWindowIndexInChild(childIndex); - int firstPeriodIndexInChild = getFirstPeriodIndexInChild(childIndex); - getChild(childIndex).getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds); + getChildDataByPeriodIndex(periodIndex, childDataHolder); + int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild; + int firstPeriodIndexInChild = childDataHolder.firstPeriodIndexInChild; + childDataHolder.timeline.getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds); period.windowIndex += firstWindowIndexInChild; if (setIds) { - period.uid = Pair.create(childIndex, period.uid); + period.uid = Pair.create(childDataHolder.uid, period.uid); } return period; } @@ -98,48 +130,40 @@ import com.google.android.exoplayer2.Timeline; if (!(uid instanceof Pair)) { return C.INDEX_UNSET; } - Pair childIndexAndPeriodId = (Pair) uid; - if (!(childIndexAndPeriodId.first instanceof Integer)) { + Pair childUidAndPeriodUid = (Pair) uid; + Object childUid = childUidAndPeriodUid.first; + Object periodUid = childUidAndPeriodUid.second; + if (!getChildDataByChildUid(childUid, childDataHolder)) { return C.INDEX_UNSET; } - int childIndex = (Integer) childIndexAndPeriodId.first; - Object periodId = childIndexAndPeriodId.second; - if (childIndex < 0 || childIndex >= getChildCount()) { - return C.INDEX_UNSET; - } - int periodIndexInChild = getChild(childIndex).getIndexOfPeriod(periodId); + int periodIndexInChild = childDataHolder.timeline.getIndexOfPeriod(periodUid); return periodIndexInChild == C.INDEX_UNSET ? C.INDEX_UNSET - : getFirstPeriodIndexInChild(childIndex) + periodIndexInChild; + : childDataHolder.firstPeriodIndexInChild + periodIndexInChild; } /** - * Returns the number of concatenated child timelines. + * Populates {@link ChildDataHolder} for the child timeline containing the given period index. + * + * @param periodIndex A valid period index within the bounds of the timeline. + * @param childData A data holder to be populated. */ - protected abstract int getChildCount(); + protected abstract void getChildDataByPeriodIndex(int periodIndex, ChildDataHolder childData); /** - * Returns a child timeline by index. + * Populates {@link ChildDataHolder} for the child timeline containing the given window index. + * + * @param windowIndex A valid window index within the bounds of the timeline. + * @param childData A data holder to be populated. */ - protected abstract Timeline getChild(int childIndex); + protected abstract void getChildDataByWindowIndex(int windowIndex, ChildDataHolder childData); /** - * Returns the index of the child timeline to which the period with the given index belongs. + * Populates {@link ChildDataHolder} for the child timeline with the given UID. + * + * @param childUid A child UID. + * @param childData A data holder to be populated. + * @return Whether a child with the given UID was found. */ - protected abstract int getChildIndexForPeriod(int periodIndex); - - /** - * Returns the first period index belonging to the child timeline with the given index. - */ - protected abstract int getFirstPeriodIndexInChild(int childIndex); - - /** - * Returns the index of the child timeline to which the window with the given index belongs. - */ - protected abstract int getChildIndexForWindow(int windowIndex); - - /** - * Returns the first window index belonging to the child timeline with the given index. - */ - protected abstract int getFirstWindowIndexInChild(int childIndex); + protected abstract boolean getChildDataByChildUid(Object childUid, ChildDataHolder childData); } 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 6263800e05..dc3b6cb1f5 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 @@ -92,7 +92,7 @@ public final class ConcatenatingMediaSource implements MediaSource { @Override public MediaPeriod createPeriod(int index, Allocator allocator) { - int sourceIndex = timeline.getChildIndexForPeriod(index); + int sourceIndex = timeline.getChildIndexByPeriodIndex(index); int periodIndexInSource = index - timeline.getFirstPeriodIndexInChild(sourceIndex); MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIndexInSource, allocator); @@ -209,32 +209,43 @@ public final class ConcatenatingMediaSource implements MediaSource { } @Override - protected int getChildCount() { - return timelines.length; + protected void getChildDataByPeriodIndex(int periodIndex, ChildDataHolder childData) { + int childIndex = getChildIndexByPeriodIndex(periodIndex); + getChildDataByChildIndex(childIndex, childData); } @Override - protected Timeline getChild(int childIndex) { - return timelines[childIndex]; + protected void getChildDataByWindowIndex(int windowIndex, ChildDataHolder childData) { + int childIndex = Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1; + getChildDataByChildIndex(childIndex, childData); } @Override - protected int getChildIndexForPeriod(int periodIndex) { + protected boolean getChildDataByChildUid(Object childUid, ChildDataHolder childData) { + if (!(childUid instanceof Integer)) { + return false; + } + int childIndex = (Integer) childUid; + getChildDataByChildIndex(childIndex, childData); + return true; + } + + private void getChildDataByChildIndex(int childIndex, ChildDataHolder childData) { + childData.timeline = timelines[childIndex]; + childData.firstPeriodIndexInChild = getFirstPeriodIndexInChild(childIndex); + childData.firstWindowIndexInChild = getFirstWindowIndexInChild(childIndex); + childData.uid = childIndex; + } + + private int getChildIndexByPeriodIndex(int periodIndex) { return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex, true, false) + 1; } - @Override - protected int getFirstPeriodIndexInChild(int childIndex) { + private int getFirstPeriodIndexInChild(int childIndex) { return childIndex == 0 ? 0 : sourcePeriodOffsets[childIndex - 1]; } - @Override - protected int getChildIndexForWindow(int windowIndex) { - return Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1; - } - - @Override - protected int getFirstWindowIndexInChild(int childIndex) { + private int getFirstWindowIndexInChild(int childIndex) { return childIndex == 0 ? 0 : sourceWindowOffsets[childIndex - 1]; } 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 240a2b9350..c663142564 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 @@ -119,34 +119,34 @@ public final class LoopingMediaSource implements MediaSource { } @Override - protected Timeline getChild(int childIndex) { - return childTimeline; + protected void getChildDataByPeriodIndex(int periodIndex, ChildDataHolder childData) { + int childIndex = periodIndex / childPeriodCount; + getChildDataByChildIndex(childIndex, childData); } @Override - protected int getChildCount() { - return loopCount; + protected void getChildDataByWindowIndex(int windowIndex, ChildDataHolder childData) { + int childIndex = windowIndex / childWindowCount; + getChildDataByChildIndex(childIndex, childData); } @Override - protected int getChildIndexForPeriod(int periodIndex) { - return periodIndex / childPeriodCount; + protected boolean getChildDataByChildUid(Object childUid, ChildDataHolder childData) { + if (!(childUid instanceof Integer)) { + return false; + } + int childIndex = (Integer) childUid; + getChildDataByChildIndex(childIndex, childData); + return true; } - @Override - protected int getFirstPeriodIndexInChild(int childIndex) { - return childIndex * childPeriodCount; + private void getChildDataByChildIndex(int childIndex, ChildDataHolder childData) { + childData.timeline = childTimeline; + childData.firstPeriodIndexInChild = childIndex * childPeriodCount; + childData.firstWindowIndexInChild = childIndex * childWindowCount; + childData.uid = childIndex; } - @Override - protected int getChildIndexForWindow(int windowIndex) { - return windowIndex / childWindowCount; - } - - @Override - protected int getFirstWindowIndexInChild(int childIndex) { - return childIndex * childWindowCount; - } } private static final class InfinitelyLoopingTimeline extends Timeline { @@ -195,5 +195,7 @@ public final class LoopingMediaSource implements MediaSource { public int getIndexOfPeriod(Object uid) { return childTimeline.getIndexOfPeriod(uid); } + } + } From fa4f876668a5a1ed8f3e945c762df3beb1777b03 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 12 Jun 2017 07:51:10 -0700 Subject: [PATCH 113/220] UI parameter to disable automatically showing playback controls. (Fixing GitHub issue #2699) Added controllerAutoShow parameter to decide whether playback controls are shown automatically. Default is true. Can be overwritten in the XML with auto_show=false or via SimpleExoPlayerView.setControllerAutoShow(false). Also inverted the logic of maybeShowControllers and showController. SimpleExoPlayerView.(show/hide)Controller and PlaybackControlView.(show/hide) now unconditionally do what they say to allow manual operation. SimpleExoPlayerView.maybeShowController is used internally to automatically show playback controls. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158712277 --- .../exoplayer2/demo/PlayerActivity.java | 2 - .../exoplayer2/ui/PlaybackControlView.java | 7 +- .../exoplayer2/ui/SimpleExoPlayerView.java | 84 ++++++++++++++++--- library/ui/src/main/res/values/attrs.xml | 1 + 4 files changed, 74 insertions(+), 20 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 71e5266ef4..a5e06fa184 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -205,8 +205,6 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay @Override public boolean dispatchKeyEvent(KeyEvent event) { - // Show the controls on any key event. - simpleExoPlayerView.showController(); // If the event was not handled then see if the player view can handle it as a media key event. return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchMediaKeyEvent(event); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 2bd576d32e..b06bbf9735 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -959,11 +959,7 @@ public class PlaybackControlView extends FrameLayout { @Override public boolean dispatchKeyEvent(KeyEvent event) { - boolean handled = dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); - if (handled) { - show(); - } - return handled; + return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); } /** @@ -1005,7 +1001,6 @@ public class PlaybackControlView extends FrameLayout { break; } } - show(); return true; } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 5cbfb638a5..3ec9b0943a 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -88,6 +88,14 @@ import java.util.List; *

    • Default: {@code true}
    • * * + *
    • {@code auto_show} - Whether the playback controls are automatically shown when + * playback starts, pauses, ends, or fails. If set to false, the playback controls can be + * manually operated with {@link #showController()} and {@link #hideController()}. + *
        + *
      • Corresponding method: {@link #setControllerAutoShow(boolean)}
      • + *
      • Default: {@code true}
      • + *
      + *
    • *
    • {@code resize_mode} - Controls how video and album art is resized within the view. * Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height} and {@code fill}. *
        @@ -198,6 +206,7 @@ public final class SimpleExoPlayerView extends FrameLayout { private boolean useArtwork; private Bitmap defaultArtwork; private int controllerShowTimeoutMs; + private boolean controllerAutoShow; private boolean controllerHideOnTouch; public SimpleExoPlayerView(Context context) { @@ -238,6 +247,7 @@ public final class SimpleExoPlayerView extends FrameLayout { int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; int controllerShowTimeoutMs = PlaybackControlView.DEFAULT_SHOW_TIMEOUT_MS; boolean controllerHideOnTouch = true; + boolean controllerAutoShow = true; if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); @@ -254,6 +264,8 @@ public final class SimpleExoPlayerView extends FrameLayout { controllerShowTimeoutMs); controllerHideOnTouch = a.getBoolean(R.styleable.SimpleExoPlayerView_hide_on_touch, controllerHideOnTouch); + controllerAutoShow = a.getBoolean(R.styleable.SimpleExoPlayerView_auto_show, + controllerAutoShow); } finally { a.recycle(); } @@ -317,6 +329,7 @@ public final class SimpleExoPlayerView extends FrameLayout { } this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0; this.controllerHideOnTouch = controllerHideOnTouch; + this.controllerAutoShow = controllerAutoShow; this.useController = useController && controller != null; hideController(); } @@ -480,6 +493,11 @@ public final class SimpleExoPlayerView extends FrameLayout { } } + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); + } + /** * Called to process media key events. Any {@link KeyEvent} can be passed but only media key * events will be handled. Does nothing if playback controls are disabled. @@ -488,16 +506,22 @@ public final class SimpleExoPlayerView extends FrameLayout { * @return Whether the key event was handled. */ public boolean dispatchMediaKeyEvent(KeyEvent event) { - return useController && controller.dispatchMediaKeyEvent(event); + boolean handled = useController && controller.dispatchMediaKeyEvent(event); + if (handled) { + maybeShowController(true); + } + return handled; } /** * Shows the playback controls. Does nothing if playback controls are disabled. + * + *

        The playback controls are automatically hidden during playback after + * {{@link #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not + * started yet, is paused, has ended or failed. */ public void showController() { - if (useController) { - maybeShowController(true); - } + showController(shouldShowControllerIndefinitely()); } /** @@ -550,6 +574,26 @@ public final class SimpleExoPlayerView extends FrameLayout { this.controllerHideOnTouch = controllerHideOnTouch; } + /** + * Returns whether the playback controls are automatically shown when playback starts, pauses, + * ends, or fails. If set to false, the playback controls can be manually operated with {@link + * #showController()} and {@link #hideController()}. + */ + public boolean getControllerAutoShow() { + return controllerAutoShow; + } + + /** + * Sets whether the playback controls are automatically shown when playback starts, pauses, ends, + * or fails. If set to false, the playback controls can be manually operated with {@link + * #showController()} and {@link #hideController()}. + * + * @param controllerAutoShow Whether the playback controls are allowed to show automatically. + */ + public void setControllerAutoShow(boolean controllerAutoShow) { + this.controllerAutoShow = controllerAutoShow; + } + /** * Set the {@link PlaybackControlView.VisibilityListener}. * @@ -664,18 +708,34 @@ public final class SimpleExoPlayerView extends FrameLayout { return true; } + /** + * Shows the playback controls, but only if forced or shown indefinitely. + */ private void maybeShowController(boolean isForced) { - if (!useController || player == null) { - return; + if (useController) { + boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0; + boolean shouldShowIndefinitely = shouldShowControllerIndefinitely(); + if (isForced || wasShowingIndefinitely || shouldShowIndefinitely) { + showController(shouldShowIndefinitely); + } + } + } + + private boolean shouldShowControllerIndefinitely() { + if (player == null) { + return true; } int playbackState = player.getPlaybackState(); - boolean showIndefinitely = playbackState == ExoPlayer.STATE_IDLE - || playbackState == ExoPlayer.STATE_ENDED || !player.getPlayWhenReady(); - boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0; - controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs); - if (isForced || showIndefinitely || wasShowingIndefinitely) { - controller.show(); + return controllerAutoShow && (playbackState == ExoPlayer.STATE_IDLE + || playbackState == ExoPlayer.STATE_ENDED || !player.getPlayWhenReady()); + } + + private void showController(boolean showIndefinitely) { + if (!useController) { + return; } + controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs); + controller.show(); } private void updateForCurrentTrackSelections() { diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 2cb28709b6..ecf3900751 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -45,6 +45,7 @@ + From 350998219acd01a1694fa1f927a1b79688ff0d0b Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 12 Jun 2017 12:12:23 -0700 Subject: [PATCH 114/220] Add test for DefaultTrackOutput ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158745843 --- .../extractor/DefaultTrackOutputTest.java | 457 ++++++++++++++++++ 1 file changed, 457 insertions(+) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultTrackOutputTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultTrackOutputTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultTrackOutputTest.java new file mode 100644 index 0000000000..f2d3076f7c --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultTrackOutputTest.java @@ -0,0 +1,457 @@ +/* + * 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.extractor; + +import android.test.MoreAsserts; +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.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 junit.framework.TestCase; + +/** + * Test for {@link DefaultTrackOutput}. + */ +public class DefaultTrackOutputTest extends TestCase { + + private static final int ALLOCATION_SIZE = 16; + + private static final Format TEST_FORMAT_1 = Format.createSampleFormat("1", "mimeType", 0); + private static final Format TEST_FORMAT_2 = Format.createSampleFormat("2", "mimeType", 0); + private static final Format TEST_FORMAT_1_COPY = Format.createSampleFormat("1", "mimeType", 0); + private static final byte[] TEST_DATA = TestUtil.buildTestData(ALLOCATION_SIZE * 10); + + /* + * TEST_SAMPLE_SIZES and TEST_SAMPLE_OFFSETS are intended to test various boundary cases (with + * respect to the allocation size). TEST_SAMPLE_OFFSETS values are defined as the backward offsets + * (as expected by DefaultTrackOutput.sampleMetadata) assuming that TEST_DATA has been written to + * the trackOutput in full. The allocations are filled as follows, where | indicates a boundary + * between allocations and x indicates a byte that doesn't belong to a sample: + * + * x|xx|x|x|||xx| + */ + private static final int[] TEST_SAMPLE_SIZES = new int[] { + ALLOCATION_SIZE - 1, ALLOCATION_SIZE - 2, ALLOCATION_SIZE - 1, ALLOCATION_SIZE - 1, + ALLOCATION_SIZE, ALLOCATION_SIZE * 2, ALLOCATION_SIZE * 2 - 2, ALLOCATION_SIZE + }; + private static final int[] TEST_SAMPLE_OFFSETS = new int[] { + ALLOCATION_SIZE * 9, ALLOCATION_SIZE * 8 + 1, ALLOCATION_SIZE * 7, ALLOCATION_SIZE * 6 + 1, + ALLOCATION_SIZE * 5, ALLOCATION_SIZE * 3, ALLOCATION_SIZE + 1, 0 + }; + private static final int[] TEST_SAMPLE_TIMESTAMPS = new int[] { + 0, 1000, 2000, 3000, 4000, 5000, 6000, 7000 + }; + private static final int[] TEST_SAMPLE_FLAGS = new int[] { + C.BUFFER_FLAG_KEY_FRAME, 0, 0, 0, C.BUFFER_FLAG_KEY_FRAME, 0, 0, 0 + }; + private static final Format[] TEST_SAMPLE_FORMATS = new Format[] { + TEST_FORMAT_1, TEST_FORMAT_1, TEST_FORMAT_1, TEST_FORMAT_1, TEST_FORMAT_2, TEST_FORMAT_2, + TEST_FORMAT_2, TEST_FORMAT_2 + }; + private static final int TEST_DATA_SECOND_KEYFRAME_INDEX = 4; + + private Allocator allocator; + private DefaultTrackOutput trackOutput; + private FormatHolder formatHolder; + private DecoderInputBuffer inputBuffer; + + @Override + public void setUp() throws Exception { + super.setUp(); + allocator = new DefaultAllocator(false, ALLOCATION_SIZE); + trackOutput = new DefaultTrackOutput(allocator); + formatHolder = new FormatHolder(); + inputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + allocator = null; + trackOutput = null; + formatHolder = null; + inputBuffer = null; + } + + public void testReadWithoutWrite() { + assertNoSamplesToRead(null); + } + + public void testReadFormatDeduplicated() { + trackOutput.format(TEST_FORMAT_1); + assertReadFormat(false, TEST_FORMAT_1); + // If the same format is input then it should be de-duplicated (i.e. not output again). + trackOutput.format(TEST_FORMAT_1); + assertNoSamplesToRead(TEST_FORMAT_1); + // The same applies for a format that's equal (but a different object). + trackOutput.format(TEST_FORMAT_1_COPY); + assertNoSamplesToRead(TEST_FORMAT_1); + } + + public void testReadSingleSamples() { + trackOutput.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE); + + assertAllocationCount(1); + // Nothing to read. + assertNoSamplesToRead(null); + + trackOutput.format(TEST_FORMAT_1); + + // Read the format. + assertReadFormat(false, TEST_FORMAT_1); + // Nothing to read. + assertNoSamplesToRead(TEST_FORMAT_1); + + trackOutput.sampleMetadata(1000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); + + // If formatRequired, should read the format rather than the sample. + assertReadFormat(true, TEST_FORMAT_1); + // Otherwise should read the sample. + assertSampleRead(1000, true, TEST_DATA, 0, ALLOCATION_SIZE); + // The allocation should have been released. + assertAllocationCount(0); + + // Nothing to read. + assertNoSamplesToRead(TEST_FORMAT_1); + + // Write a second sample followed by one byte that does not belong to it. + trackOutput.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE); + trackOutput.sampleMetadata(2000, 0, ALLOCATION_SIZE - 1, 1, null); + + // If formatRequired, should read the format rather than the sample. + assertReadFormat(true, TEST_FORMAT_1); + // Read the sample. + assertSampleRead(2000, false, TEST_DATA, 0, ALLOCATION_SIZE - 1); + // The last byte written to the output may belong to a sample whose metadata has yet to be + // written, so an allocation should still be held. + assertAllocationCount(1); + + // Write metadata for a third sample containing the remaining byte. + trackOutput.sampleMetadata(3000, 0, 1, 0, null); + + // If formatRequired, should read the format rather than the sample. + assertReadFormat(true, TEST_FORMAT_1); + // Read the sample. + assertSampleRead(3000, false, TEST_DATA, ALLOCATION_SIZE - 1, 1); + // The allocation should have been released. + assertAllocationCount(0); + } + + public void testReadMultiSamples() { + writeTestData(); + assertEquals(TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1], + trackOutput.getLargestQueuedTimestampUs()); + assertAllocationCount(10); + assertReadTestData(); + assertAllocationCount(0); + } + + public void testReadMultiSamplesTwice() { + writeTestData(); + writeTestData(); + assertAllocationCount(20); + assertReadTestData(TEST_FORMAT_2); + assertReadTestData(TEST_FORMAT_2); + assertAllocationCount(0); + } + + public void testSkipAll() { + writeTestData(); + trackOutput.skipAll(); + assertAllocationCount(0); + // Despite skipping all samples, we should still read the last format, since this is the + // expected format for a subsequent sample. + assertReadFormat(false, TEST_FORMAT_2); + // Once the format has been read, there's nothing else to read. + assertNoSamplesToRead(TEST_FORMAT_2); + } + + public void testSkipAllRetainsUnassignedData() { + trackOutput.format(TEST_FORMAT_1); + trackOutput.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE); + trackOutput.skipAll(); + // Skipping shouldn't discard data that may belong to a sample whose metadata has yet to be + // written. + assertAllocationCount(1); + // We should be able to read the format. + assertReadFormat(false, TEST_FORMAT_1); + // Once the format has been read, there's nothing else to read. + assertNoSamplesToRead(TEST_FORMAT_1); + + trackOutput.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); + // Once the metadata has been written, check the sample can be read as expected. + assertSampleRead(0, true, TEST_DATA, 0, ALLOCATION_SIZE); + assertNoSamplesToRead(TEST_FORMAT_1); + assertAllocationCount(0); + } + + public void testSkipToKeyframeBeforeBuffer() { + writeTestData(); + boolean result = trackOutput.skipToKeyframeBefore(TEST_SAMPLE_TIMESTAMPS[0] - 1, false); + // Should fail and have no effect. + assertFalse(result); + assertReadTestData(); + assertNoSamplesToRead(TEST_FORMAT_2); + } + + public void testSkipToKeyframeStartOfBuffer() { + writeTestData(); + boolean result = trackOutput.skipToKeyframeBefore(TEST_SAMPLE_TIMESTAMPS[0], false); + // Should succeed but have no effect (we're already at the first frame). + assertTrue(result); + assertReadTestData(); + assertNoSamplesToRead(TEST_FORMAT_2); + } + + public void testSkipToKeyframeEndOfBuffer() { + writeTestData(); + boolean result = trackOutput.skipToKeyframeBefore( + TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1], false); + // Should succeed and skip to 2nd keyframe. + assertTrue(result); + assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX); + assertNoSamplesToRead(TEST_FORMAT_2); + } + + public void testSkipToKeyframeAfterBuffer() { + writeTestData(); + boolean result = trackOutput.skipToKeyframeBefore( + TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1] + 1, false); + // Should fail and have no effect. + assertFalse(result); + assertReadTestData(); + assertNoSamplesToRead(TEST_FORMAT_2); + } + + public void testSkipToKeyframeAfterBufferAllowed() { + writeTestData(); + boolean result = trackOutput.skipToKeyframeBefore( + TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1] + 1, true); + // Should succeed and skip to 2nd keyframe. + assertTrue(result); + assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX); + assertNoSamplesToRead(TEST_FORMAT_2); + } + + // Internal methods. + + /** + * Writes standard test data to {@code trackOutput}. + */ + @SuppressWarnings("ReferenceEquality") + private void writeTestData() { + trackOutput.sampleData(new ParsableByteArray(TEST_DATA), TEST_DATA.length); + Format format = null; + for (int i = 0; i < TEST_SAMPLE_TIMESTAMPS.length; i++) { + if (TEST_SAMPLE_FORMATS[i] != format) { + trackOutput.format(TEST_SAMPLE_FORMATS[i]); + format = TEST_SAMPLE_FORMATS[i]; + } + trackOutput.sampleMetadata(TEST_SAMPLE_TIMESTAMPS[i], TEST_SAMPLE_FLAGS[i], + TEST_SAMPLE_SIZES[i], TEST_SAMPLE_OFFSETS[i], null); + } + } + + /** + * Asserts correct reading of standard test data from {@code trackOutput}. + */ + private void assertReadTestData() { + assertReadTestData(null, 0); + } + + /** + * Asserts correct reading of standard test data from {@code trackOutput}. + * + * @param startFormat The format of the last sample previously read from {@code trackOutput}. + */ + private void assertReadTestData(Format startFormat) { + assertReadTestData(startFormat, 0); + } + + /** + * Asserts correct reading of standard test data from {@code trackOutput}. + * + * @param startFormat The format of the last sample previously read from {@code trackOutput}. + * @param firstSampleIndex The index of the first sample that's expected to be read. + */ + private void assertReadTestData(Format startFormat, int firstSampleIndex) { + Format format = startFormat; + for (int i = firstSampleIndex; i < TEST_SAMPLE_TIMESTAMPS.length; i++) { + // Use equals() on the read side despite using referential equality on the write side, since + // trackOutput de-duplicates written formats using equals(). + if (!TEST_SAMPLE_FORMATS[i].equals(format)) { + // If the format has changed, we should read it. + assertReadFormat(false, TEST_SAMPLE_FORMATS[i]); + format = TEST_SAMPLE_FORMATS[i]; + } + // If we require the format, we should always read it. + assertReadFormat(true, TEST_SAMPLE_FORMATS[i]); + // Assert the sample is as expected. + assertSampleRead(TEST_SAMPLE_TIMESTAMPS[i], + (TEST_SAMPLE_FLAGS[i] & C.BUFFER_FLAG_KEY_FRAME) != 0, + TEST_DATA, + TEST_DATA.length - TEST_SAMPLE_OFFSETS[i] - TEST_SAMPLE_SIZES[i], + TEST_SAMPLE_SIZES[i]); + } + } + + /** + * Asserts {@link DefaultTrackOutput#readData} is behaving correctly, given there are no samples + * to read and the last format to be written to the output is {@code endFormat}. + * + * @param endFormat The last format to be written to the output, or null of no format has been + * written. + */ + private void assertNoSamplesToRead(Format endFormat) { + // If not formatRequired or loadingFinished, should read nothing. + assertReadNothing(false); + // If formatRequired, should read the end format if set, else read nothing. + if (endFormat == null) { + assertReadNothing(true); + } else { + assertReadFormat(true, endFormat); + } + // If loadingFinished, should read end of stream. + assertReadEndOfStream(false); + assertReadEndOfStream(true); + // Having read end of stream should not affect other cases. + assertReadNothing(false); + if (endFormat == null) { + assertReadNothing(true); + } else { + assertReadFormat(true, endFormat); + } + } + + /** + * Asserts {@link DefaultTrackOutput#readData} returns {@link C#RESULT_NOTHING_READ}. + * + * @param formatRequired The value of {@code formatRequired} passed to readData. + */ + private void assertReadNothing(boolean formatRequired) { + clearFormatHolderAndInputBuffer(); + int result = trackOutput.readData(formatHolder, inputBuffer, formatRequired, false, 0); + assertEquals(C.RESULT_NOTHING_READ, result); + // formatHolder should not be populated. + assertNull(formatHolder.format); + // inputBuffer should not be populated. + assertInputBufferContainsNoSampleData(); + assertInputBufferHasNoDefaultFlagsSet(); + } + + /** + * Asserts {@link DefaultTrackOutput#readData} returns {@link C#RESULT_BUFFER_READ} and that the + * {@link DecoderInputBuffer#isEndOfStream()} is set. + * + * @param formatRequired The value of {@code formatRequired} passed to readData. + */ + private void assertReadEndOfStream(boolean formatRequired) { + clearFormatHolderAndInputBuffer(); + int result = trackOutput.readData(formatHolder, inputBuffer, formatRequired, true, 0); + assertEquals(C.RESULT_BUFFER_READ, result); + // formatHolder should not be populated. + assertNull(formatHolder.format); + // inputBuffer should not contain sample data, but end of stream flag should be set. + assertInputBufferContainsNoSampleData(); + assertTrue(inputBuffer.isEndOfStream()); + assertFalse(inputBuffer.isDecodeOnly()); + assertFalse(inputBuffer.isEncrypted()); + } + + /** + * Asserts {@link DefaultTrackOutput#readData} 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 format The expected format. + */ + private void assertReadFormat(boolean formatRequired, Format format) { + clearFormatHolderAndInputBuffer(); + int result = trackOutput.readData(formatHolder, inputBuffer, formatRequired, false, 0); + assertEquals(C.RESULT_FORMAT_READ, result); + // formatHolder should be populated. + assertEquals(format, formatHolder.format); + // inputBuffer should not be populated. + assertInputBufferContainsNoSampleData(); + assertInputBufferHasNoDefaultFlagsSet(); + } + + /** + * Asserts {@link DefaultTrackOutput#readData} returns {@link C#RESULT_BUFFER_READ} and that the + * buffer is filled with the specified sample data. + * + * @param timeUs The expected buffer timestamp. + * @param isKeyframe The expected keyframe flag. + * @param sampleData An array containing the expected sample data. + * @param offset The offset in {@code sampleData} of the expected sample data. + * @param length The length of the expected sample data. + */ + private void assertSampleRead(long timeUs, boolean isKeyframe, byte[] sampleData, int offset, + int length) { + clearFormatHolderAndInputBuffer(); + int result = trackOutput.readData(formatHolder, inputBuffer, false, false, 0); + assertEquals(C.RESULT_BUFFER_READ, result); + // formatHolder should not be populated. + assertNull(formatHolder.format); + // inputBuffer should be populated. + assertEquals(timeUs, inputBuffer.timeUs); + assertEquals(isKeyframe, inputBuffer.isKeyFrame()); + assertFalse(inputBuffer.isDecodeOnly()); + assertFalse(inputBuffer.isEncrypted()); + inputBuffer.flip(); + assertEquals(length, inputBuffer.data.limit()); + byte[] readData = new byte[length]; + inputBuffer.data.get(readData); + MoreAsserts.assertEquals(Arrays.copyOfRange(sampleData, offset, offset + length), readData); + } + + /** + * Asserts the number of allocations currently in use by {@code trackOutput}. + * + * @param count The expected number of allocations. + */ + private void assertAllocationCount(int count) { + assertEquals(ALLOCATION_SIZE * count, allocator.getTotalBytesAllocated()); + } + + /** + * Asserts {@code inputBuffer} does not contain any sample data. + */ + private void assertInputBufferContainsNoSampleData() { + if (inputBuffer.data == null) { + return; + } + inputBuffer.flip(); + assertEquals(0, inputBuffer.data.limit()); + } + + private void assertInputBufferHasNoDefaultFlagsSet() { + assertFalse(inputBuffer.isEndOfStream()); + assertFalse(inputBuffer.isDecodeOnly()); + assertFalse(inputBuffer.isEncrypted()); + } + + private void clearFormatHolderAndInputBuffer() { + formatHolder.format = null; + inputBuffer.clear(); + } + +} From 0f5c30d3453c5faa32fe1378a8b38f125e834a9e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 13 Jun 2017 13:35:10 +0100 Subject: [PATCH 115/220] Misc cleanup --- .../video/VideoFrameReleaseTimeHelper.java | 19 ++++++++++--------- .../exoplayer2/ui/DebugTextViewHelper.java | 13 ++++++------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java index 32c7325547..bd9f749e31 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.C; @TargetApi(16) public final class VideoFrameReleaseTimeHelper { + private static final double DISPLAY_REFRESH_RATE_UNKNOWN = -1; private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500; private static final long MAX_ALLOWED_DRIFT_NS = 20000000; @@ -52,11 +53,11 @@ public final class VideoFrameReleaseTimeHelper { private long frameCount; /** - * Constructs an instance that smoothes frame release timestamps but does not align them with + * Constructs an instance that smooths frame release timestamps but does not align them with * the default display's vsync signal. */ public VideoFrameReleaseTimeHelper() { - this(-1 /* Value unused */, false); + this(DISPLAY_REFRESH_RATE_UNKNOWN); } /** @@ -66,13 +67,12 @@ public final class VideoFrameReleaseTimeHelper { * @param context A context from which information about the default display can be retrieved. */ public VideoFrameReleaseTimeHelper(Context context) { - this(getDefaultDisplayRefreshRate(context), true); + this(getDefaultDisplayRefreshRate(context)); } - private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate, - boolean useDefaultDisplayVsync) { - this.useDefaultDisplayVsync = useDefaultDisplayVsync; - if (useDefaultDisplayVsync && defaultDisplayRefreshRate > 0f) { + private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate) { + useDefaultDisplayVsync = defaultDisplayRefreshRate != DISPLAY_REFRESH_RATE_UNKNOWN; + if (useDefaultDisplayVsync) { vsyncSampler = VSyncSampler.getInstance(); vsyncDurationNs = (long) (C.NANOS_PER_SECOND / defaultDisplayRefreshRate); vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100; @@ -200,9 +200,10 @@ public final class VideoFrameReleaseTimeHelper { return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs; } - private static float getDefaultDisplayRefreshRate(Context context) { + private static double getDefaultDisplayRefreshRate(Context context) { WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - return manager.getDefaultDisplay() != null ? manager.getDefaultDisplay().getRefreshRate() : 0f; + return manager.getDefaultDisplay() != null ? manager.getDefaultDisplay().getRefreshRate() + : DISPLAY_REFRESH_RATE_UNKNOWN; } /** diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index e65b475c97..373312b073 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -163,14 +163,8 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe if (format == null) { return ""; } - float par = format.pixelWidthHeightRatio; - String parInfo = ""; - if (par != Format.NO_VALUE && (int) par != 1) { - // Add pixel aspect ratio only when it's useful - parInfo = " par:" + format.pixelWidthHeightRatio; - } return "\n" + format.sampleMimeType + "(id:" + format.id + " r:" + format.width + "x" - + format.height + parInfo + + format.height + getPixelAspectRatioString(format.pixelWidthHeightRatio) + getDecoderCountersBufferCountString(player.getVideoDecoderCounters()) + ")"; } @@ -195,4 +189,9 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe + " mcdb:" + counters.maxConsecutiveDroppedOutputBufferCount; } + private static String getPixelAspectRatioString(float pixelAspectRatio) { + return pixelAspectRatio == Format.NO_VALUE || pixelAspectRatio == 1f ? "" + : (" par:" + String.format("%.02f", pixelAspectRatio)); + } + } From 0c1212b3099ff14177dc52f34985bd2831488c34 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 13 Jun 2017 14:10:31 +0100 Subject: [PATCH 116/220] Fix typo --- .../android/exoplayer2/video/VideoFrameReleaseTimeHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java index bd9f749e31..ad489c2312 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java @@ -61,7 +61,7 @@ public final class VideoFrameReleaseTimeHelper { } /** - * Constructs an instance that smoothes frame release timestamps and aligns them with the default + * Constructs an instance that smooths frame release timestamps and aligns them with the default * display's vsync signal. * * @param context A context from which information about the default display can be retrieved. From fb12a659a2e06a70a4dc8db4f793068ce8a60f1b Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 13 Jun 2017 06:19:59 -0700 Subject: [PATCH 117/220] Fix discarding upstream from DefaultTrackOutput ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158837777 --- .../extractor/DefaultTrackOutputTest.java | 76 ++++++++++++++++++- .../extractor/SampleMetadataQueue.java | 4 +- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultTrackOutputTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultTrackOutputTest.java index f2d3076f7c..bffba73070 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultTrackOutputTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultTrackOutputTest.java @@ -251,6 +251,68 @@ public class DefaultTrackOutputTest extends TestCase { assertNoSamplesToRead(TEST_FORMAT_2); } + public void testDiscardUpstream() { + writeTestData(); + trackOutput.discardUpstreamSamples(8); + assertAllocationCount(10); + trackOutput.discardUpstreamSamples(7); + assertAllocationCount(9); + trackOutput.discardUpstreamSamples(6); + assertAllocationCount(8); // Byte not belonging to sample prevents 7. + trackOutput.discardUpstreamSamples(5); + assertAllocationCount(5); + trackOutput.discardUpstreamSamples(4); + assertAllocationCount(4); + trackOutput.discardUpstreamSamples(3); + assertAllocationCount(3); + trackOutput.discardUpstreamSamples(2); + assertAllocationCount(3); // Byte not belonging to sample prevents 2. + trackOutput.discardUpstreamSamples(1); + assertAllocationCount(2); // Byte not belonging to sample prevents 1. + trackOutput.discardUpstreamSamples(0); + assertAllocationCount(1); // Byte not belonging to sample prevents 0. + assertReadFormat(false, TEST_FORMAT_2); + assertNoSamplesToRead(TEST_FORMAT_2); + } + + public void testDiscardUpstreamMulti() { + writeTestData(); + trackOutput.discardUpstreamSamples(4); + assertAllocationCount(4); + trackOutput.discardUpstreamSamples(0); + assertAllocationCount(1); // Byte not belonging to sample prevents 0. + assertReadFormat(false, TEST_FORMAT_2); + assertNoSamplesToRead(TEST_FORMAT_2); + } + + public void testDiscardUpstreamBeforeRead() { + writeTestData(); + trackOutput.discardUpstreamSamples(4); + assertAllocationCount(4); + assertReadTestData(null, 0, 4); + assertReadFormat(false, TEST_FORMAT_2); + assertNoSamplesToRead(TEST_FORMAT_2); + } + + public void testDiscardUpstreamAfterRead() { + writeTestData(); + assertReadTestData(null, 0, 3); + trackOutput.discardUpstreamSamples(8); + assertAllocationCount(7); + trackOutput.discardUpstreamSamples(7); + assertAllocationCount(6); + trackOutput.discardUpstreamSamples(6); + assertAllocationCount(5); // Byte not belonging to sample prevents 4. + trackOutput.discardUpstreamSamples(5); + assertAllocationCount(2); + trackOutput.discardUpstreamSamples(4); + assertAllocationCount(1); + trackOutput.discardUpstreamSamples(3); + assertAllocationCount(0); + assertReadFormat(false, TEST_FORMAT_2); + assertNoSamplesToRead(TEST_FORMAT_2); + } + // Internal methods. /** @@ -293,8 +355,20 @@ public class DefaultTrackOutputTest extends TestCase { * @param firstSampleIndex The index of the first sample that's expected to be read. */ private void assertReadTestData(Format startFormat, int firstSampleIndex) { + assertReadTestData(startFormat, firstSampleIndex, + TEST_SAMPLE_TIMESTAMPS.length - firstSampleIndex); + } + + /** + * Asserts correct reading of standard test data from {@code trackOutput}. + * + * @param startFormat The format of the last sample previously read from {@code trackOutput}. + * @param firstSampleIndex The index of the first sample that's expected to be read. + * @param sampleCount The number of samples to read. + */ + private void assertReadTestData(Format startFormat, int firstSampleIndex, int sampleCount) { Format format = startFormat; - for (int i = firstSampleIndex; i < TEST_SAMPLE_TIMESTAMPS.length; i++) { + for (int i = firstSampleIndex; i < firstSampleIndex + sampleCount; i++) { // Use equals() on the read side despite using referential equality on the write side, since // trackOutput de-duplicates written formats using equals(). if (!TEST_SAMPLE_FORMATS[i].equals(format)) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SampleMetadataQueue.java index 452b540e53..40aaa295a0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SampleMetadataQueue.java @@ -111,8 +111,8 @@ import com.google.android.exoplayer2.util.Util; Assertions.checkArgument(0 <= discardCount && discardCount <= length); if (discardCount == 0) { - if (absoluteStartIndex == 0) { - // length == absoluteStartIndex == 0, so nothing has been written to the queue. + if (absoluteStartIndex == 0 && length == 0) { + // Nothing has been written to the queue. return 0; } int lastWriteIndex = (relativeEndIndex == 0 ? capacity : relativeEndIndex) - 1; From f3e9166a4eabeb7b2ec8cffedf7c4914a91f48d5 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 13 Jun 2017 06:34:02 -0700 Subject: [PATCH 118/220] Use DummySurface on S8 where possible ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158838707 --- .../exoplayer2/video/DummySurface.java | 62 ++++++++++++++----- .../video/MediaCodecVideoRenderer.java | 22 ++----- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index e998eceaaf..e32f23fed7 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 @@ -41,6 +41,8 @@ import static android.opengl.GLES20.glDeleteTextures; import static android.opengl.GLES20.glGenTextures; import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.PackageManager; import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture.OnFrameAvailableListener; import android.opengl.EGL14; @@ -68,19 +70,8 @@ public final class DummySurface extends Surface { private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; - /** - * Whether the device supports secure dummy surfaces. - */ - public static final boolean SECURE_SUPPORTED; - static { - if (Util.SDK_INT >= 17) { - EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); - String extensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); - SECURE_SUPPORTED = extensions != null && extensions.contains("EGL_EXT_protected_content"); - } else { - SECURE_SUPPORTED = false; - } - } + private static boolean secureSupported; + private static boolean secureSupportedInitialized; /** * Whether the surface is secure. @@ -90,18 +81,40 @@ public final class DummySurface extends Surface { private final DummySurfaceThread thread; private boolean threadReleased; + /** + * Returns whether the device supports secure dummy surfaces. + * + * @param context Any {@link Context}. + * @return Whether the device supports secure dummy surfaces. + */ + public static synchronized boolean isSecureSupported(Context context) { + if (!secureSupportedInitialized) { + if (Util.SDK_INT >= 17) { + EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + String extensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); + secureSupported = extensions != null && extensions.contains("EGL_EXT_protected_content") + && !deviceNeedsSecureDummySurfaceWorkaround(context); + } + secureSupportedInitialized = true; + } + return secureSupported; + } + /** * Returns a newly created dummy surface. The surface must be released by calling {@link #release} * when it's no longer required. *

        * Must only be called if {@link Util#SDK_INT} is 17 or higher. * + * @param context Any {@link Context}. * @param secure Whether a secure surface is required. Must only be requested if - * {@link #SECURE_SUPPORTED} is {@code true}. + * {@link #isSecureSupported(Context)} returns {@code true}. + * @throws IllegalStateException If a secure surface is requested on a device for which + * {@link #isSecureSupported(Context)} returns {@code false}. */ - public static DummySurface newInstanceV17(boolean secure) { + public static DummySurface newInstanceV17(Context context, boolean secure) { assertApiLevel17OrHigher(); - Assertions.checkState(!secure || SECURE_SUPPORTED); + Assertions.checkState(!secure || isSecureSupported(context)); DummySurfaceThread thread = new DummySurfaceThread(); return thread.init(secure); } @@ -133,6 +146,23 @@ public final class DummySurface extends Surface { } } + /** + * Returns whether the device is known to advertise secure surface textures but not implement them + * correctly. + * + * @param context Any {@link Context}. + */ + private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { + return Util.SDK_INT == 24 + && (Util.MODEL.startsWith("SM-G950") || Util.MODEL.startsWith("SM-G955")) + && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager()); + } + + @TargetApi(24) + private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) { + return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE); + } + private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, Callback { 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 7610bb1a55..75e10f05ff 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 @@ -67,6 +67,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // pending output streams that have fewer frames than the codec latency. private static final int MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT = 10; + private final Context context; private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper; private final EventDispatcher eventDispatcher; private final long allowedJoiningTimeMs; @@ -167,6 +168,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { super(C.TRACK_TYPE_VIDEO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); this.allowedJoiningTimeMs = allowedJoiningTimeMs; this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; + this.context = context.getApplicationContext(); frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context); eventDispatcher = new EventDispatcher(eventHandler, eventListener); deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround(); @@ -341,7 +343,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } else { MediaCodecInfo codecInfo = getCodecInfo(); if (codecInfo != null && shouldUseDummySurface(codecInfo.secure)) { - dummySurface = DummySurface.newInstanceV17(codecInfo.secure); + dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure); surface = dummySurface; } } @@ -394,7 +396,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (surface == null) { Assertions.checkState(shouldUseDummySurface(codecInfo.secure)); if (dummySurface == null) { - dummySurface = DummySurface.newInstanceV17(codecInfo.secure); + dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure); } surface = dummySurface; } @@ -653,8 +655,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } private boolean shouldUseDummySurface(boolean codecIsSecure) { - return Util.SDK_INT >= 23 && !tunneling && (!codecIsSecure - || (DummySurface.SECURE_SUPPORTED && !deviceNeedsSecureDummySurfaceWorkaround())); + return Util.SDK_INT >= 23 && !tunneling + && (!codecIsSecure || DummySurface.isSecureSupported(context)); } private void setJoiningDeadlineMs() { @@ -921,18 +923,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { codec.setVideoScalingMode(scalingMode); } - /** - * Returns whether the device is known to fail outputting from a secure decoder to a secure - * surface texture. - *

        - * If true is returned then use of {@link DummySurface} is disabled for secure playbacks. - */ - private static boolean deviceNeedsSecureDummySurfaceWorkaround() { - // See [Internal: b/37197802]. - return Util.SDK_INT == 24 - && (Util.MODEL.startsWith("SM-G950") || Util.MODEL.startsWith("SM-G955")); - } - /** * Returns whether the device is known to enable frame-rate conversion logic that negatively * impacts ExoPlayer. From 6362dfeb986b9f672c745349f674feba96c8ac12 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 13 Jun 2017 06:43:20 -0700 Subject: [PATCH 119/220] Replace LinkedBlockingDeque with our own linked list This will allow us to maintain a reference to the middle of the queue, which is necessary to efficiently support decoupling the read position from the start of the buffer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158839336 --- .../extractor/DefaultTrackOutputTest.java | 7 + .../extractor/DefaultTrackOutput.java | 181 +++++++++++++----- 2 files changed, 145 insertions(+), 43 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultTrackOutputTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultTrackOutputTest.java index bffba73070..9c3c22ed87 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultTrackOutputTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultTrackOutputTest.java @@ -91,6 +91,13 @@ public class DefaultTrackOutputTest extends TestCase { inputBuffer = null; } + public void testDisableReleasesAllocations() { + writeTestData(); + assertAllocationCount(10); + trackOutput.disable(); + assertAllocationCount(0); + } + public void testReadWithoutWrite() { assertNoSamplesToRead(null); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java index d627f3fe3c..c768b06277 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; @@ -26,7 +27,6 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.EOFException; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.atomic.AtomicInteger; /** @@ -57,15 +57,16 @@ public final class DefaultTrackOutput implements TrackOutput { private final Allocator allocator; private final int allocationLength; - private final SampleMetadataQueue metadataQueue; - private final LinkedBlockingDeque dataQueue; private final SampleExtrasHolder extrasHolder; private final ParsableByteArray scratch; private final AtomicInteger state; + // References into the linked list of allocations. + private AllocationNode firstAllocationNode; + private AllocationNode writeAllocationNode; + // Accessed only by the consuming thread. - private long totalBytesDropped; private Format downstreamFormat; // Accessed only by the loading thread (or the consuming thread when there is no loading thread). @@ -73,7 +74,6 @@ public final class DefaultTrackOutput implements TrackOutput { private Format lastUnadjustedFormat; private long sampleOffsetUs; private long totalBytesWritten; - private Allocation lastAllocation; private int lastAllocationOffset; private boolean pendingSplice; private UpstreamFormatChangedListener upstreamFormatChangeListener; @@ -85,11 +85,12 @@ public final class DefaultTrackOutput implements TrackOutput { this.allocator = allocator; allocationLength = allocator.getIndividualAllocationLength(); metadataQueue = new SampleMetadataQueue(); - dataQueue = new LinkedBlockingDeque<>(); extrasHolder = new SampleExtrasHolder(); scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); state = new AtomicInteger(); lastAllocationOffset = allocationLength; + firstAllocationNode = new AllocationNode(0, allocationLength); + writeAllocationNode = firstAllocationNode; } // Called by the consuming thread, but only when there is no loading thread. @@ -149,23 +150,23 @@ public final class DefaultTrackOutput implements TrackOutput { * @param absolutePosition The absolute position (inclusive) from which to discard data. */ private void dropUpstreamFrom(long absolutePosition) { - int relativePosition = (int) (absolutePosition - totalBytesDropped); - // Calculate the index of the allocation containing the position, and the offset within it. - int allocationIndex = relativePosition / allocationLength; - int allocationOffset = relativePosition % allocationLength; - // We want to discard any allocations after the one at allocationIdnex. - int allocationDiscardCount = dataQueue.size() - allocationIndex - 1; - if (allocationOffset == 0) { - // If the allocation at allocationIndex is empty, we should discard that one too. - allocationDiscardCount++; + if (absolutePosition == firstAllocationNode.startPosition) { + clearAllocationNodes(firstAllocationNode); + firstAllocationNode = new AllocationNode(absolutePosition, allocationLength); + writeAllocationNode = firstAllocationNode; + } else { + AllocationNode newWriteAllocationNode = firstAllocationNode; + AllocationNode currentNode = firstAllocationNode.next; + while (absolutePosition > currentNode.startPosition) { + newWriteAllocationNode = currentNode; + currentNode = currentNode.next; + } + clearAllocationNodes(currentNode); + writeAllocationNode = newWriteAllocationNode; + writeAllocationNode.next = new AllocationNode(writeAllocationNode.endPosition, + allocationLength); + lastAllocationOffset = (int) (absolutePosition - writeAllocationNode.startPosition); } - // Discard the allocations. - for (int i = 0; i < allocationDiscardCount; i++) { - allocator.release(dataQueue.removeLast()); - } - // Update lastAllocation and lastAllocationOffset to reflect the new position. - lastAllocation = dataQueue.peekLast(); - lastAllocationOffset = allocationOffset == 0 ? allocationLength : allocationOffset; } // Called by the consuming thread. @@ -384,14 +385,18 @@ public final class DefaultTrackOutput implements TrackOutput { */ private void readData(long absolutePosition, ByteBuffer target, int length) { int remaining = length; + dropDownstreamTo(absolutePosition); while (remaining > 0) { - dropDownstreamTo(absolutePosition); - int positionInAllocation = (int) (absolutePosition - totalBytesDropped); + int positionInAllocation = (int) (absolutePosition - firstAllocationNode.startPosition); int toCopy = Math.min(remaining, allocationLength - positionInAllocation); - Allocation allocation = dataQueue.peek(); + Allocation allocation = firstAllocationNode.allocation; target.put(allocation.data, allocation.translateOffset(positionInAllocation), toCopy); absolutePosition += toCopy; remaining -= toCopy; + if (absolutePosition == firstAllocationNode.endPosition) { + allocator.release(allocation); + firstAllocationNode = firstAllocationNode.clear(); + } } } @@ -404,15 +409,19 @@ public final class DefaultTrackOutput implements TrackOutput { */ private void readData(long absolutePosition, byte[] target, int length) { int bytesRead = 0; + dropDownstreamTo(absolutePosition); while (bytesRead < length) { - dropDownstreamTo(absolutePosition); - int positionInAllocation = (int) (absolutePosition - totalBytesDropped); + int positionInAllocation = (int) (absolutePosition - firstAllocationNode.startPosition); int toCopy = Math.min(length - bytesRead, allocationLength - positionInAllocation); - Allocation allocation = dataQueue.peek(); + Allocation allocation = firstAllocationNode.allocation; System.arraycopy(allocation.data, allocation.translateOffset(positionInAllocation), target, bytesRead, toCopy); absolutePosition += toCopy; bytesRead += toCopy; + if (absolutePosition == firstAllocationNode.endPosition) { + allocator.release(allocation); + firstAllocationNode = firstAllocationNode.clear(); + } } } @@ -423,11 +432,9 @@ public final class DefaultTrackOutput implements TrackOutput { * @param absolutePosition The absolute position up to which allocations can be discarded. */ private void dropDownstreamTo(long absolutePosition) { - int relativePosition = (int) (absolutePosition - totalBytesDropped); - int allocationIndex = relativePosition / allocationLength; - for (int i = 0; i < allocationIndex; i++) { - allocator.release(dataQueue.remove()); - totalBytesDropped += allocationLength; + while (absolutePosition >= firstAllocationNode.endPosition) { + allocator.release(firstAllocationNode.allocation); + firstAllocationNode = firstAllocationNode.clear(); } } @@ -481,8 +488,9 @@ public final class DefaultTrackOutput implements TrackOutput { } try { length = prepareForAppend(length); - int bytesAppended = input.read(lastAllocation.data, - lastAllocation.translateOffset(lastAllocationOffset), length); + Allocation writeAllocation = writeAllocationNode.allocation; + int bytesAppended = input.read(writeAllocation.data, + writeAllocation.translateOffset(lastAllocationOffset), length); if (bytesAppended == C.RESULT_END_OF_INPUT) { if (allowEndOfInput) { return C.RESULT_END_OF_INPUT; @@ -505,7 +513,8 @@ public final class DefaultTrackOutput implements TrackOutput { } while (length > 0) { int thisAppendLength = prepareForAppend(length); - buffer.readBytes(lastAllocation.data, lastAllocation.translateOffset(lastAllocationOffset), + Allocation writeAllocation = writeAllocationNode.allocation; + buffer.readBytes(writeAllocation.data, writeAllocation.translateOffset(lastAllocationOffset), thisAppendLength); lastAllocationOffset += thisAppendLength; totalBytesWritten += thisAppendLength; @@ -553,13 +562,35 @@ public final class DefaultTrackOutput implements TrackOutput { private void clearSampleData() { metadataQueue.clearSampleData(); - allocator.release(dataQueue.toArray(new Allocation[dataQueue.size()])); - dataQueue.clear(); - allocator.trim(); - totalBytesDropped = 0; + clearAllocationNodes(firstAllocationNode); + firstAllocationNode = new AllocationNode(0, allocationLength); + writeAllocationNode = firstAllocationNode; totalBytesWritten = 0; - lastAllocation = null; lastAllocationOffset = allocationLength; + allocator.trim(); + } + + /** + * Clears allocation nodes starting from {@code fromNode}. + * + * @param fromNode The node from which to clear. + */ + private void clearAllocationNodes(AllocationNode fromNode) { + if (!fromNode.wasInitialized) { + return; + } + // Bulk release allocations for performance (it's significantly faster when using + // DefaultAllocator because the allocator's lock only needs to be acquired and released once) + // [Internal: See b/29542039]. + int allocationCount = (writeAllocationNode.wasInitialized ? 1 : 0) + + ((int) (writeAllocationNode.startPosition - fromNode.startPosition) / allocationLength); + Allocation[] allocationsToRelease = new Allocation[allocationCount]; + AllocationNode currentNode = fromNode; + for (int i = 0; i < allocationsToRelease.length; i++) { + allocationsToRelease[i] = currentNode.allocation; + currentNode = currentNode.clear(); + } + allocator.release(allocationsToRelease); } /** @@ -569,8 +600,11 @@ public final class DefaultTrackOutput implements TrackOutput { private int prepareForAppend(int length) { if (lastAllocationOffset == allocationLength) { lastAllocationOffset = 0; - lastAllocation = allocator.allocate(); - dataQueue.add(lastAllocation); + if (writeAllocationNode.wasInitialized) { + writeAllocationNode = writeAllocationNode.next; + } + writeAllocationNode.initialize(allocator.allocate(), + new AllocationNode(writeAllocationNode.endPosition, allocationLength)); } return Math.min(length, allocationLength - lastAllocationOffset); } @@ -592,4 +626,65 @@ public final class DefaultTrackOutput implements TrackOutput { return format; } + /** + * A node in a linked list of {@link Allocation}s held by the output. + */ + private static final class AllocationNode { + + /** + * The absolute position of the start of the data (inclusive). + */ + public final long startPosition; + /** + * The absolute position of the end of the data (exclusive). + */ + public final long endPosition; + /** + * Whether the node has been initialized. Remains true after {@link #clear()}. + */ + public boolean wasInitialized; + /** + * The {@link Allocation}, or {@code null} if the node is not initialized. + */ + @Nullable public Allocation allocation; + /** + * The next {@link AllocationNode} in the list, or {@code null} if the node has not been + * initialized. Remains set after {@link #clear()}. + */ + @Nullable public AllocationNode next; + + /** + * @param startPosition See {@link #startPosition}. + * @param allocationLength The length of the {@link Allocation} with which this node will be + * initialized. + */ + public AllocationNode(long startPosition, int allocationLength) { + this.startPosition = startPosition; + this.endPosition = startPosition + allocationLength; + } + + /** + * Initializes the node. + * + * @param allocation The node's {@link Allocation}. + * @param next The next {@link AllocationNode}. + */ + public void initialize(Allocation allocation, AllocationNode next) { + this.allocation = allocation; + this.next = next; + wasInitialized = true; + } + + /** + * Clears {@link #allocation}. + * + * @return The next {@link AllocationNode}, for convenience. + */ + public AllocationNode clear() { + allocation = null; + return next; + } + + } + } From e4617567a3ce23980319d79e961e5043343edf4e Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 13 Jun 2017 07:26:53 -0700 Subject: [PATCH 120/220] Rename DefaultTrackOutput to SampleQueue ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158842843 --- .../SampleQueueTest.java} | 147 +++++++++--------- .../source/ExtractorMediaPeriod.java | 11 +- .../SampleMetadataQueue.java | 2 +- .../SampleQueue.java} | 13 +- .../source/chunk/BaseMediaChunkOutput.java | 30 ++-- .../source/chunk/ChunkSampleStream.java | 30 ++-- .../source/hls/HlsSampleStreamWrapper.java | 20 +-- 7 files changed, 126 insertions(+), 127 deletions(-) rename library/core/src/androidTest/java/com/google/android/exoplayer2/{extractor/DefaultTrackOutputTest.java => source/SampleQueueTest.java} (80%) rename library/core/src/main/java/com/google/android/exoplayer2/{extractor => source}/SampleMetadataQueue.java (99%) rename library/core/src/main/java/com/google/android/exoplayer2/{extractor/DefaultTrackOutput.java => source/SampleQueue.java} (98%) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultTrackOutputTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java similarity index 80% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultTrackOutputTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 9c3c22ed87..129f299779 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultTrackOutputTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.extractor; +package com.google.android.exoplayer2.source; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; @@ -28,9 +28,9 @@ import java.util.Arrays; import junit.framework.TestCase; /** - * Test for {@link DefaultTrackOutput}. + * Test for {@link SampleQueue}. */ -public class DefaultTrackOutputTest extends TestCase { +public class SampleQueueTest extends TestCase { private static final int ALLOCATION_SIZE = 16; @@ -42,8 +42,8 @@ public class DefaultTrackOutputTest extends TestCase { /* * TEST_SAMPLE_SIZES and TEST_SAMPLE_OFFSETS are intended to test various boundary cases (with * respect to the allocation size). TEST_SAMPLE_OFFSETS values are defined as the backward offsets - * (as expected by DefaultTrackOutput.sampleMetadata) assuming that TEST_DATA has been written to - * the trackOutput in full. The allocations are filled as follows, where | indicates a boundary + * (as expected by SampleQueue.sampleMetadata) assuming that TEST_DATA has been written to the + * sampleQueue in full. The allocations are filled as follows, where | indicates a boundary * between allocations and x indicates a byte that doesn't belong to a sample: * * x|xx|x|x|||xx| @@ -69,7 +69,7 @@ public class DefaultTrackOutputTest extends TestCase { private static final int TEST_DATA_SECOND_KEYFRAME_INDEX = 4; private Allocator allocator; - private DefaultTrackOutput trackOutput; + private SampleQueue sampleQueue; private FormatHolder formatHolder; private DecoderInputBuffer inputBuffer; @@ -77,7 +77,7 @@ public class DefaultTrackOutputTest extends TestCase { public void setUp() throws Exception { super.setUp(); allocator = new DefaultAllocator(false, ALLOCATION_SIZE); - trackOutput = new DefaultTrackOutput(allocator); + sampleQueue = new SampleQueue(allocator); formatHolder = new FormatHolder(); inputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); } @@ -86,7 +86,7 @@ public class DefaultTrackOutputTest extends TestCase { public void tearDown() throws Exception { super.tearDown(); allocator = null; - trackOutput = null; + sampleQueue = null; formatHolder = null; inputBuffer = null; } @@ -94,7 +94,7 @@ public class DefaultTrackOutputTest extends TestCase { public void testDisableReleasesAllocations() { writeTestData(); assertAllocationCount(10); - trackOutput.disable(); + sampleQueue.disable(); assertAllocationCount(0); } @@ -103,31 +103,31 @@ public class DefaultTrackOutputTest extends TestCase { } public void testReadFormatDeduplicated() { - trackOutput.format(TEST_FORMAT_1); + sampleQueue.format(TEST_FORMAT_1); assertReadFormat(false, TEST_FORMAT_1); // If the same format is input then it should be de-duplicated (i.e. not output again). - trackOutput.format(TEST_FORMAT_1); + sampleQueue.format(TEST_FORMAT_1); assertNoSamplesToRead(TEST_FORMAT_1); // The same applies for a format that's equal (but a different object). - trackOutput.format(TEST_FORMAT_1_COPY); + sampleQueue.format(TEST_FORMAT_1_COPY); assertNoSamplesToRead(TEST_FORMAT_1); } public void testReadSingleSamples() { - trackOutput.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE); + sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE); assertAllocationCount(1); // Nothing to read. assertNoSamplesToRead(null); - trackOutput.format(TEST_FORMAT_1); + sampleQueue.format(TEST_FORMAT_1); // Read the format. assertReadFormat(false, TEST_FORMAT_1); // Nothing to read. assertNoSamplesToRead(TEST_FORMAT_1); - trackOutput.sampleMetadata(1000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); + sampleQueue.sampleMetadata(1000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); // If formatRequired, should read the format rather than the sample. assertReadFormat(true, TEST_FORMAT_1); @@ -140,19 +140,19 @@ public class DefaultTrackOutputTest extends TestCase { assertNoSamplesToRead(TEST_FORMAT_1); // Write a second sample followed by one byte that does not belong to it. - trackOutput.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE); - trackOutput.sampleMetadata(2000, 0, ALLOCATION_SIZE - 1, 1, null); + sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE); + sampleQueue.sampleMetadata(2000, 0, ALLOCATION_SIZE - 1, 1, null); // If formatRequired, should read the format rather than the sample. assertReadFormat(true, TEST_FORMAT_1); // Read the sample. assertSampleRead(2000, false, TEST_DATA, 0, ALLOCATION_SIZE - 1); - // The last byte written to the output may belong to a sample whose metadata has yet to be + // The last byte written to the sample queue may belong to a sample whose metadata has yet to be // written, so an allocation should still be held. assertAllocationCount(1); // Write metadata for a third sample containing the remaining byte. - trackOutput.sampleMetadata(3000, 0, 1, 0, null); + sampleQueue.sampleMetadata(3000, 0, 1, 0, null); // If formatRequired, should read the format rather than the sample. assertReadFormat(true, TEST_FORMAT_1); @@ -165,7 +165,7 @@ public class DefaultTrackOutputTest extends TestCase { public void testReadMultiSamples() { writeTestData(); assertEquals(TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1], - trackOutput.getLargestQueuedTimestampUs()); + sampleQueue.getLargestQueuedTimestampUs()); assertAllocationCount(10); assertReadTestData(); assertAllocationCount(0); @@ -182,7 +182,7 @@ public class DefaultTrackOutputTest extends TestCase { public void testSkipAll() { writeTestData(); - trackOutput.skipAll(); + sampleQueue.skipAll(); assertAllocationCount(0); // Despite skipping all samples, we should still read the last format, since this is the // expected format for a subsequent sample. @@ -192,9 +192,9 @@ public class DefaultTrackOutputTest extends TestCase { } public void testSkipAllRetainsUnassignedData() { - trackOutput.format(TEST_FORMAT_1); - trackOutput.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE); - trackOutput.skipAll(); + sampleQueue.format(TEST_FORMAT_1); + sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE); + sampleQueue.skipAll(); // Skipping shouldn't discard data that may belong to a sample whose metadata has yet to be // written. assertAllocationCount(1); @@ -203,7 +203,7 @@ public class DefaultTrackOutputTest extends TestCase { // Once the format has been read, there's nothing else to read. assertNoSamplesToRead(TEST_FORMAT_1); - trackOutput.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); + sampleQueue.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); // Once the metadata has been written, check the sample can be read as expected. assertSampleRead(0, true, TEST_DATA, 0, ALLOCATION_SIZE); assertNoSamplesToRead(TEST_FORMAT_1); @@ -212,7 +212,7 @@ public class DefaultTrackOutputTest extends TestCase { public void testSkipToKeyframeBeforeBuffer() { writeTestData(); - boolean result = trackOutput.skipToKeyframeBefore(TEST_SAMPLE_TIMESTAMPS[0] - 1, false); + boolean result = sampleQueue.skipToKeyframeBefore(TEST_SAMPLE_TIMESTAMPS[0] - 1, false); // Should fail and have no effect. assertFalse(result); assertReadTestData(); @@ -221,7 +221,7 @@ public class DefaultTrackOutputTest extends TestCase { public void testSkipToKeyframeStartOfBuffer() { writeTestData(); - boolean result = trackOutput.skipToKeyframeBefore(TEST_SAMPLE_TIMESTAMPS[0], false); + boolean result = sampleQueue.skipToKeyframeBefore(TEST_SAMPLE_TIMESTAMPS[0], false); // Should succeed but have no effect (we're already at the first frame). assertTrue(result); assertReadTestData(); @@ -230,7 +230,7 @@ public class DefaultTrackOutputTest extends TestCase { public void testSkipToKeyframeEndOfBuffer() { writeTestData(); - boolean result = trackOutput.skipToKeyframeBefore( + boolean result = sampleQueue.skipToKeyframeBefore( TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1], false); // Should succeed and skip to 2nd keyframe. assertTrue(result); @@ -240,7 +240,7 @@ public class DefaultTrackOutputTest extends TestCase { public void testSkipToKeyframeAfterBuffer() { writeTestData(); - boolean result = trackOutput.skipToKeyframeBefore( + boolean result = sampleQueue.skipToKeyframeBefore( TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1] + 1, false); // Should fail and have no effect. assertFalse(result); @@ -250,7 +250,7 @@ public class DefaultTrackOutputTest extends TestCase { public void testSkipToKeyframeAfterBufferAllowed() { writeTestData(); - boolean result = trackOutput.skipToKeyframeBefore( + boolean result = sampleQueue.skipToKeyframeBefore( TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1] + 1, true); // Should succeed and skip to 2nd keyframe. assertTrue(result); @@ -260,23 +260,23 @@ public class DefaultTrackOutputTest extends TestCase { public void testDiscardUpstream() { writeTestData(); - trackOutput.discardUpstreamSamples(8); + sampleQueue.discardUpstreamSamples(8); assertAllocationCount(10); - trackOutput.discardUpstreamSamples(7); + sampleQueue.discardUpstreamSamples(7); assertAllocationCount(9); - trackOutput.discardUpstreamSamples(6); + sampleQueue.discardUpstreamSamples(6); assertAllocationCount(8); // Byte not belonging to sample prevents 7. - trackOutput.discardUpstreamSamples(5); + sampleQueue.discardUpstreamSamples(5); assertAllocationCount(5); - trackOutput.discardUpstreamSamples(4); + sampleQueue.discardUpstreamSamples(4); assertAllocationCount(4); - trackOutput.discardUpstreamSamples(3); + sampleQueue.discardUpstreamSamples(3); assertAllocationCount(3); - trackOutput.discardUpstreamSamples(2); + sampleQueue.discardUpstreamSamples(2); assertAllocationCount(3); // Byte not belonging to sample prevents 2. - trackOutput.discardUpstreamSamples(1); + sampleQueue.discardUpstreamSamples(1); assertAllocationCount(2); // Byte not belonging to sample prevents 1. - trackOutput.discardUpstreamSamples(0); + sampleQueue.discardUpstreamSamples(0); assertAllocationCount(1); // Byte not belonging to sample prevents 0. assertReadFormat(false, TEST_FORMAT_2); assertNoSamplesToRead(TEST_FORMAT_2); @@ -284,9 +284,9 @@ public class DefaultTrackOutputTest extends TestCase { public void testDiscardUpstreamMulti() { writeTestData(); - trackOutput.discardUpstreamSamples(4); + sampleQueue.discardUpstreamSamples(4); assertAllocationCount(4); - trackOutput.discardUpstreamSamples(0); + sampleQueue.discardUpstreamSamples(0); assertAllocationCount(1); // Byte not belonging to sample prevents 0. assertReadFormat(false, TEST_FORMAT_2); assertNoSamplesToRead(TEST_FORMAT_2); @@ -294,7 +294,7 @@ public class DefaultTrackOutputTest extends TestCase { public void testDiscardUpstreamBeforeRead() { writeTestData(); - trackOutput.discardUpstreamSamples(4); + sampleQueue.discardUpstreamSamples(4); assertAllocationCount(4); assertReadTestData(null, 0, 4); assertReadFormat(false, TEST_FORMAT_2); @@ -304,17 +304,17 @@ public class DefaultTrackOutputTest extends TestCase { public void testDiscardUpstreamAfterRead() { writeTestData(); assertReadTestData(null, 0, 3); - trackOutput.discardUpstreamSamples(8); + sampleQueue.discardUpstreamSamples(8); assertAllocationCount(7); - trackOutput.discardUpstreamSamples(7); + sampleQueue.discardUpstreamSamples(7); assertAllocationCount(6); - trackOutput.discardUpstreamSamples(6); + sampleQueue.discardUpstreamSamples(6); assertAllocationCount(5); // Byte not belonging to sample prevents 4. - trackOutput.discardUpstreamSamples(5); + sampleQueue.discardUpstreamSamples(5); assertAllocationCount(2); - trackOutput.discardUpstreamSamples(4); + sampleQueue.discardUpstreamSamples(4); assertAllocationCount(1); - trackOutput.discardUpstreamSamples(3); + sampleQueue.discardUpstreamSamples(3); assertAllocationCount(0); assertReadFormat(false, TEST_FORMAT_2); assertNoSamplesToRead(TEST_FORMAT_2); @@ -323,43 +323,42 @@ public class DefaultTrackOutputTest extends TestCase { // Internal methods. /** - * Writes standard test data to {@code trackOutput}. + * Writes standard test data to {@code sampleQueue}. */ @SuppressWarnings("ReferenceEquality") private void writeTestData() { - trackOutput.sampleData(new ParsableByteArray(TEST_DATA), TEST_DATA.length); + sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), TEST_DATA.length); Format format = null; for (int i = 0; i < TEST_SAMPLE_TIMESTAMPS.length; i++) { if (TEST_SAMPLE_FORMATS[i] != format) { - trackOutput.format(TEST_SAMPLE_FORMATS[i]); + sampleQueue.format(TEST_SAMPLE_FORMATS[i]); format = TEST_SAMPLE_FORMATS[i]; } - trackOutput.sampleMetadata(TEST_SAMPLE_TIMESTAMPS[i], TEST_SAMPLE_FLAGS[i], + sampleQueue.sampleMetadata(TEST_SAMPLE_TIMESTAMPS[i], TEST_SAMPLE_FLAGS[i], TEST_SAMPLE_SIZES[i], TEST_SAMPLE_OFFSETS[i], null); } } /** - * Asserts correct reading of standard test data from {@code trackOutput}. + * Asserts correct reading of standard test data from {@code sampleQueue}. */ private void assertReadTestData() { assertReadTestData(null, 0); } /** - * Asserts correct reading of standard test data from {@code trackOutput}. + * Asserts correct reading of standard test data from {@code sampleQueue}. * - * @param startFormat The format of the last sample previously read from {@code trackOutput}. + * @param startFormat The format of the last sample previously read from {@code sampleQueue}. */ private void assertReadTestData(Format startFormat) { assertReadTestData(startFormat, 0); } /** - * Asserts correct reading of standard test data from {@code trackOutput}. + * Asserts correct reading of standard test data from {@code sampleQueue}. * - * @param startFormat The format of the last sample previously read from {@code trackOutput}. - * @param firstSampleIndex The index of the first sample that's expected to be read. + * @param startFormat The format of the last sample previously read from {@code sampleQueue}. */ private void assertReadTestData(Format startFormat, int firstSampleIndex) { assertReadTestData(startFormat, firstSampleIndex, @@ -367,9 +366,9 @@ public class DefaultTrackOutputTest extends TestCase { } /** - * Asserts correct reading of standard test data from {@code trackOutput}. + * Asserts correct reading of standard test data from {@code sampleQueue}. * - * @param startFormat The format of the last sample previously read from {@code trackOutput}. + * @param startFormat The format of the last sample previously read from {@code sampleQueue}. * @param firstSampleIndex The index of the first sample that's expected to be read. * @param sampleCount The number of samples to read. */ @@ -377,7 +376,7 @@ public class DefaultTrackOutputTest extends TestCase { Format format = startFormat; for (int i = firstSampleIndex; i < firstSampleIndex + sampleCount; i++) { // Use equals() on the read side despite using referential equality on the write side, since - // trackOutput de-duplicates written formats using equals(). + // sampleQueue de-duplicates written formats using equals(). if (!TEST_SAMPLE_FORMATS[i].equals(format)) { // If the format has changed, we should read it. assertReadFormat(false, TEST_SAMPLE_FORMATS[i]); @@ -395,11 +394,11 @@ public class DefaultTrackOutputTest extends TestCase { } /** - * Asserts {@link DefaultTrackOutput#readData} is behaving correctly, given there are no samples - * to read and the last format to be written to the output is {@code endFormat}. + * Asserts {@link SampleQueue#readData} is behaving correctly, given there are no samples + * to read and the last format to be written to the sample queue is {@code endFormat}. * - * @param endFormat The last format to be written to the output, or null of no format has been - * written. + * @param endFormat The last format to be written to the sample queue, or null of no format has + * been written. */ private void assertNoSamplesToRead(Format endFormat) { // If not formatRequired or loadingFinished, should read nothing. @@ -423,13 +422,13 @@ public class DefaultTrackOutputTest extends TestCase { } /** - * Asserts {@link DefaultTrackOutput#readData} returns {@link C#RESULT_NOTHING_READ}. + * Asserts {@link SampleQueue#readData} returns {@link C#RESULT_NOTHING_READ}. * * @param formatRequired The value of {@code formatRequired} passed to readData. */ private void assertReadNothing(boolean formatRequired) { clearFormatHolderAndInputBuffer(); - int result = trackOutput.readData(formatHolder, inputBuffer, formatRequired, false, 0); + int result = sampleQueue.readData(formatHolder, inputBuffer, formatRequired, false, 0); assertEquals(C.RESULT_NOTHING_READ, result); // formatHolder should not be populated. assertNull(formatHolder.format); @@ -439,14 +438,14 @@ public class DefaultTrackOutputTest extends TestCase { } /** - * Asserts {@link DefaultTrackOutput#readData} returns {@link C#RESULT_BUFFER_READ} and that the + * Asserts {@link SampleQueue#readData} returns {@link C#RESULT_BUFFER_READ} and that the * {@link DecoderInputBuffer#isEndOfStream()} is set. * * @param formatRequired The value of {@code formatRequired} passed to readData. */ private void assertReadEndOfStream(boolean formatRequired) { clearFormatHolderAndInputBuffer(); - int result = trackOutput.readData(formatHolder, inputBuffer, formatRequired, true, 0); + int result = sampleQueue.readData(formatHolder, inputBuffer, formatRequired, true, 0); assertEquals(C.RESULT_BUFFER_READ, result); // formatHolder should not be populated. assertNull(formatHolder.format); @@ -458,7 +457,7 @@ public class DefaultTrackOutputTest extends TestCase { } /** - * Asserts {@link DefaultTrackOutput#readData} returns {@link C#RESULT_FORMAT_READ} and that the + * Asserts {@link SampleQueue#readData} 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. @@ -466,7 +465,7 @@ public class DefaultTrackOutputTest extends TestCase { */ private void assertReadFormat(boolean formatRequired, Format format) { clearFormatHolderAndInputBuffer(); - int result = trackOutput.readData(formatHolder, inputBuffer, formatRequired, false, 0); + int result = sampleQueue.readData(formatHolder, inputBuffer, formatRequired, false, 0); assertEquals(C.RESULT_FORMAT_READ, result); // formatHolder should be populated. assertEquals(format, formatHolder.format); @@ -476,7 +475,7 @@ public class DefaultTrackOutputTest extends TestCase { } /** - * Asserts {@link DefaultTrackOutput#readData} returns {@link C#RESULT_BUFFER_READ} and that the + * Asserts {@link SampleQueue#readData} returns {@link C#RESULT_BUFFER_READ} and that the * buffer is filled with the specified sample data. * * @param timeUs The expected buffer timestamp. @@ -488,7 +487,7 @@ public class DefaultTrackOutputTest extends TestCase { private void assertSampleRead(long timeUs, boolean isKeyframe, byte[] sampleData, int offset, int length) { clearFormatHolderAndInputBuffer(); - int result = trackOutput.readData(formatHolder, inputBuffer, false, false, 0); + int result = sampleQueue.readData(formatHolder, inputBuffer, false, false, 0); assertEquals(C.RESULT_BUFFER_READ, result); // formatHolder should not be populated. assertNull(formatHolder.format); @@ -505,7 +504,7 @@ public class DefaultTrackOutputTest extends TestCase { } /** - * Asserts the number of allocations currently in use by {@code trackOutput}. + * Asserts the number of allocations currently in use by {@code sampleQueue}. * * @param count The expected number of allocations. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 8eaa9cae5a..62b1e85456 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -23,14 +23,13 @@ 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.DefaultExtractorInput; -import com.google.android.exoplayer2.extractor.DefaultTrackOutput; -import com.google.android.exoplayer2.extractor.DefaultTrackOutput.UpstreamFormatChangedListener; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; 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.source.SampleQueue.UpstreamFormatChangedListener; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; @@ -71,7 +70,7 @@ import java.io.IOException; private final Runnable maybeFinishPrepareRunnable; private final Runnable onContinueLoadingRequestedRunnable; private final Handler handler; - private final SparseArray sampleQueues; + private final SparseArray sampleQueues; private Callback callback; private SeekMap seekMap; @@ -345,7 +344,7 @@ import java.io.IOException; } /* package */ void skipData(int track, long positionUs) { - DefaultTrackOutput sampleQueue = sampleQueues.valueAt(track); + SampleQueue sampleQueue = sampleQueues.valueAt(track); if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { sampleQueue.skipAll(); } else { @@ -402,9 +401,9 @@ import java.io.IOException; @Override public TrackOutput track(int id, int type) { - DefaultTrackOutput trackOutput = sampleQueues.get(id); + SampleQueue trackOutput = sampleQueues.get(id); if (trackOutput == null) { - trackOutput = new DefaultTrackOutput(allocator); + trackOutput = new SampleQueue(allocator); trackOutput.setUpstreamFormatChangeListener(this); sampleQueues.put(id, trackOutput); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java similarity index 99% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/SampleMetadataQueue.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index 40aaa295a0..a114c6eae3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.extractor; +package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java similarity index 98% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index c768b06277..fc72fac364 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -13,14 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.extractor; +package com.google.android.exoplayer2.source; import android.support.annotation.Nullable; 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.SampleMetadataQueue.SampleExtrasHolder; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.source.SampleMetadataQueue.SampleExtrasHolder; import com.google.android.exoplayer2.upstream.Allocation; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -30,10 +32,9 @@ import java.nio.ByteBuffer; import java.util.concurrent.atomic.AtomicInteger; /** - * A {@link TrackOutput} that buffers extracted samples in a queue and allows for consumption from - * that queue. + * A queue of media samples. */ -public final class DefaultTrackOutput implements TrackOutput { +public final class SampleQueue implements TrackOutput { /** * A listener for changes to the upstream format. @@ -81,7 +82,7 @@ public final class DefaultTrackOutput implements TrackOutput { /** * @param allocator An {@link Allocator} from which allocations for sample data can be obtained. */ - public DefaultTrackOutput(Allocator allocator) { + public SampleQueue(Allocator allocator) { this.allocator = allocator; allocationLength = allocator.getIndividualAllocationLength(); metadataQueue = new SampleMetadataQueue(); 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 3882a330f9..9531aaf32e 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 @@ -16,9 +16,9 @@ package com.google.android.exoplayer2.source.chunk; import android.util.Log; -import com.google.android.exoplayer2.extractor.DefaultTrackOutput; 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; /** @@ -29,22 +29,22 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut private static final String TAG = "BaseMediaChunkOutput"; private final int[] trackTypes; - private final DefaultTrackOutput[] trackOutputs; + private final SampleQueue[] sampleQueues; /** * @param trackTypes The track types of the individual track outputs. - * @param trackOutputs The individual track outputs. + * @param sampleQueues The individual sample queues. */ - public BaseMediaChunkOutput(int[] trackTypes, DefaultTrackOutput[] trackOutputs) { + public BaseMediaChunkOutput(int[] trackTypes, SampleQueue[] sampleQueues) { this.trackTypes = trackTypes; - this.trackOutputs = trackOutputs; + this.sampleQueues = sampleQueues; } @Override public TrackOutput track(int id, int type) { for (int i = 0; i < trackTypes.length; i++) { if (type == trackTypes[i]) { - return trackOutputs[i]; + return sampleQueues[i]; } } Log.e(TAG, "Unmatched track of type: " + type); @@ -52,13 +52,13 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut } /** - * Returns the current absolute write indices of the individual track outputs. + * Returns the current absolute write indices of the individual sample queues. */ public int[] getWriteIndices() { - int[] writeIndices = new int[trackOutputs.length]; - for (int i = 0; i < trackOutputs.length; i++) { - if (trackOutputs[i] != null) { - writeIndices[i] = trackOutputs[i].getWriteIndex(); + int[] writeIndices = new int[sampleQueues.length]; + for (int i = 0; i < sampleQueues.length; i++) { + if (sampleQueues[i] != null) { + writeIndices[i] = sampleQueues[i].getWriteIndex(); } } return writeIndices; @@ -66,12 +66,12 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut /** * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples - * subsequently written to the track outputs. + * subsequently written to the sample queues. */ public void setSampleOffsetUs(long sampleOffsetUs) { - for (DefaultTrackOutput trackOutput : trackOutputs) { - if (trackOutput != null) { - trackOutput.setSampleOffsetUs(sampleOffsetUs); + for (SampleQueue sampleQueue : sampleQueues) { + if (sampleQueue != null) { + sampleQueue.setSampleOffsetUs(sampleOffsetUs); } } } 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 b4399f0f81..d7c9174a89 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 @@ -19,8 +19,8 @@ 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.DefaultTrackOutput; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +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; @@ -49,8 +49,8 @@ public class ChunkSampleStream implements SampleStream, S private final ChunkHolder nextChunkHolder; private final LinkedList mediaChunks; private final List readOnlyMediaChunks; - private final DefaultTrackOutput primarySampleQueue; - private final DefaultTrackOutput[] embeddedSampleQueues; + private final SampleQueue primarySampleQueue; + private final SampleQueue[] embeddedSampleQueues; private final BaseMediaChunkOutput mediaChunkOutput; private Format primaryDownstreamTrackFormat; @@ -85,19 +85,19 @@ public class ChunkSampleStream implements SampleStream, S readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; - embeddedSampleQueues = new DefaultTrackOutput[embeddedTrackCount]; + embeddedSampleQueues = new SampleQueue[embeddedTrackCount]; embeddedTracksSelected = new boolean[embeddedTrackCount]; int[] trackTypes = new int[1 + embeddedTrackCount]; - DefaultTrackOutput[] sampleQueues = new DefaultTrackOutput[1 + embeddedTrackCount]; + SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount]; - primarySampleQueue = new DefaultTrackOutput(allocator); + primarySampleQueue = new SampleQueue(allocator); trackTypes[0] = primaryTrackType; sampleQueues[0] = primarySampleQueue; for (int i = 0; i < embeddedTrackCount; i++) { - DefaultTrackOutput trackOutput = new DefaultTrackOutput(allocator); - embeddedSampleQueues[i] = trackOutput; - sampleQueues[i + 1] = trackOutput; + SampleQueue sampleQueue = new SampleQueue(allocator); + embeddedSampleQueues[i] = sampleQueue; + sampleQueues[i + 1] = sampleQueue; trackTypes[i + 1] = embeddedTrackTypes[i]; } @@ -192,7 +192,7 @@ public class ChunkSampleStream implements SampleStream, S } // TODO: For this to work correctly, the embedded streams must not discard anything from their // sample queues beyond the current read position of the primary stream. - for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { embeddedSampleQueue.skipToKeyframeBefore(positionUs, true); } } else { @@ -204,7 +204,7 @@ public class ChunkSampleStream implements SampleStream, S loader.cancelLoading(); } else { primarySampleQueue.reset(true); - for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { embeddedSampleQueue.reset(true); } } @@ -218,7 +218,7 @@ public class ChunkSampleStream implements SampleStream, S */ public void release() { primarySampleQueue.disable(); - for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { embeddedSampleQueue.disable(); } loader.release(); @@ -280,7 +280,7 @@ public class ChunkSampleStream implements SampleStream, S loadable.bytesLoaded()); if (!released) { primarySampleQueue.reset(true); - for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { embeddedSampleQueue.reset(true); } callback.onContinueLoadingRequested(this); @@ -439,10 +439,10 @@ public class ChunkSampleStream implements SampleStream, S public final ChunkSampleStream parent; - private final DefaultTrackOutput sampleQueue; + private final SampleQueue sampleQueue; private final int index; - public EmbeddedSampleStream(ChunkSampleStream parent, DefaultTrackOutput sampleQueue, + public EmbeddedSampleStream(ChunkSampleStream parent, SampleQueue sampleQueue, int index) { this.parent = parent; this.sampleQueue = sampleQueue; 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 a73553263b..0e8567b846 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 @@ -22,11 +22,11 @@ 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.DefaultTrackOutput; -import com.google.android.exoplayer2.extractor.DefaultTrackOutput.UpstreamFormatChangedListener; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.SampleQueue; +import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; @@ -81,7 +81,7 @@ import java.util.LinkedList; private final Loader loader; private final EventDispatcher eventDispatcher; private final HlsChunkSource.HlsChunkHolder nextChunkHolder; - private final SparseArray sampleQueues; + private final SparseArray sampleQueues; private final LinkedList mediaChunks; private final Runnable maybeFinishPrepareRunnable; private final Handler handler; @@ -315,7 +315,7 @@ import java.util.LinkedList; } /* package */ void skipData(int group, long positionUs) { - DefaultTrackOutput sampleQueue = sampleQueues.valueAt(group); + SampleQueue sampleQueue = sampleQueues.valueAt(group); if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { sampleQueue.skipAll(); } else { @@ -471,15 +471,15 @@ import java.util.LinkedList; // ExtractorOutput implementation. Called by the loading thread. @Override - public DefaultTrackOutput track(int id, int type) { + public SampleQueue track(int id, int type) { if (sampleQueues.indexOfKey(id) >= 0) { return sampleQueues.get(id); } - DefaultTrackOutput trackOutput = new DefaultTrackOutput(allocator); - trackOutput.setUpstreamFormatChangeListener(this); - trackOutput.sourceId(upstreamChunkUid); - sampleQueues.put(id, trackOutput); - return trackOutput; + SampleQueue sampleQueue = new SampleQueue(allocator); + sampleQueue.setUpstreamFormatChangeListener(this); + sampleQueue.sourceId(upstreamChunkUid); + sampleQueues.put(id, sampleQueue); + return sampleQueue; } @Override From 810c120abcb10567f26f6ae2c8b4f22f2cb9a211 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 14 Jun 2017 03:34:19 -0700 Subject: [PATCH 121/220] Increase MP3 sniffing distance Issue: #2951 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158960483 --- .../google/android/exoplayer2/extractor/mp3/Mp3Extractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6e114137f1..8d33f95640 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 @@ -78,7 +78,7 @@ public final class Mp3Extractor implements Extractor { /** * The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up. */ - private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES; + private static final int MAX_SNIFF_BYTES = 16 * 1024; /** * Maximum length of data read into {@link #scratch}. */ From cdcdf1d37c84f4436d6aeee998fdf2590b8f7b81 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 14 Jun 2017 07:56:37 -0700 Subject: [PATCH 122/220] Log frame counts when we see a spurious audio timestamp ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158977741 --- .../java/com/google/android/exoplayer2/audio/AudioTrack.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 44a96373f3..92838e34b0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -1292,7 +1292,7 @@ public final class AudioTrack { // The timestamp time base is probably wrong. String message = "Spurious audio timestamp (system clock mismatch): " + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", " - + playbackPositionUs; + + playbackPositionUs + ", " + getSubmittedFrames() + ", " + getWrittenFrames(); if (failOnSpuriousAudioTimestamp) { throw new InvalidAudioTrackTimestampException(message); } @@ -1303,7 +1303,7 @@ public final class AudioTrack { // The timestamp frame position is probably wrong. String message = "Spurious audio timestamp (frame position mismatch): " + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", " - + playbackPositionUs; + + playbackPositionUs + ", " + getSubmittedFrames() + ", " + getWrittenFrames(); if (failOnSpuriousAudioTimestamp) { throw new InvalidAudioTrackTimestampException(message); } From a913fd952fff38c9e9978a7734a10cdab7c348c3 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 15 Jun 2017 02:36:46 -0700 Subject: [PATCH 123/220] Add support for mono input to the GVR extension Issue: #2710 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159082518 --- extensions/gvr/build.gradle | 2 +- .../google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index f622a73758..e15c8b1ad8 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -25,7 +25,7 @@ android { dependencies { compile project(':library-core') - compile 'com.google.vr:sdk-audio:1.30.0' + compile 'com.google.vr:sdk-audio:1.60.1' } ext { diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java index 980424904d..a56bc7f0a9 100644 --- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java +++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java @@ -82,6 +82,9 @@ public final class GvrAudioProcessor implements AudioProcessor { maybeReleaseGvrAudioSurround(); int surroundFormat; switch (channelCount) { + case 1: + surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO; + break; case 2: surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO; break; From 2a353a834dc8571e805cb8c7c6a7fdbd149b33a4 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 15 Jun 2017 07:06:38 -0700 Subject: [PATCH 124/220] Advance SampleQueue writeAllocationNode earlier Previously, writeAllocationNode was not advanced to the terminating node when finishing writing sample data that fills exactly up to the end of the current write node. This wasn't actually broken, but is confusing because it causes edge cases where the start/read references could temporarily refer the node after the current write node. This change advances the write reference in this case, removing this confusion and bringing the implementation in line with what the design doc says happens. Also making some simplification and consistency changes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159099522 --- .../exoplayer2/source/SampleQueue.java | 87 +++++++++++-------- .../exoplayer2/upstream/Allocation.java | 19 ++-- 2 files changed, 56 insertions(+), 50 deletions(-) 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 fc72fac364..2551307004 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 @@ -75,7 +75,6 @@ public final class SampleQueue implements TrackOutput { private Format lastUnadjustedFormat; private long sampleOffsetUs; private long totalBytesWritten; - private int lastAllocationOffset; private boolean pendingSplice; private UpstreamFormatChangedListener upstreamFormatChangeListener; @@ -89,7 +88,6 @@ public final class SampleQueue implements TrackOutput { extrasHolder = new SampleExtrasHolder(); scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); state = new AtomicInteger(); - lastAllocationOffset = allocationLength; firstAllocationNode = new AllocationNode(0, allocationLength); writeAllocationNode = firstAllocationNode; } @@ -166,7 +164,6 @@ public final class SampleQueue implements TrackOutput { writeAllocationNode = newWriteAllocationNode; writeAllocationNode.next = new AllocationNode(writeAllocationNode.endPosition, allocationLength); - lastAllocationOffset = (int) (absolutePosition - writeAllocationNode.startPosition); } } @@ -388,12 +385,11 @@ public final class SampleQueue implements TrackOutput { int remaining = length; dropDownstreamTo(absolutePosition); while (remaining > 0) { - int positionInAllocation = (int) (absolutePosition - firstAllocationNode.startPosition); - int toCopy = Math.min(remaining, allocationLength - positionInAllocation); + int toCopy = Math.min(remaining, (int) (firstAllocationNode.endPosition - absolutePosition)); Allocation allocation = firstAllocationNode.allocation; - target.put(allocation.data, allocation.translateOffset(positionInAllocation), toCopy); - absolutePosition += toCopy; + target.put(allocation.data, firstAllocationNode.translateOffset(absolutePosition), toCopy); remaining -= toCopy; + absolutePosition += toCopy; if (absolutePosition == firstAllocationNode.endPosition) { allocator.release(allocation); firstAllocationNode = firstAllocationNode.clear(); @@ -409,16 +405,15 @@ public final class SampleQueue implements TrackOutput { * @param length The number of bytes to read. */ private void readData(long absolutePosition, byte[] target, int length) { - int bytesRead = 0; + int remaining = length; dropDownstreamTo(absolutePosition); - while (bytesRead < length) { - int positionInAllocation = (int) (absolutePosition - firstAllocationNode.startPosition); - int toCopy = Math.min(length - bytesRead, allocationLength - positionInAllocation); + while (remaining > 0) { + int toCopy = Math.min(remaining, (int) (firstAllocationNode.endPosition - absolutePosition)); Allocation allocation = firstAllocationNode.allocation; - System.arraycopy(allocation.data, allocation.translateOffset(positionInAllocation), target, - bytesRead, toCopy); + System.arraycopy(allocation.data, firstAllocationNode.translateOffset(absolutePosition), + target, length - remaining, toCopy); + remaining -= toCopy; absolutePosition += toCopy; - bytesRead += toCopy; if (absolutePosition == firstAllocationNode.endPosition) { allocator.release(allocation); firstAllocationNode = firstAllocationNode.clear(); @@ -488,18 +483,16 @@ public final class SampleQueue implements TrackOutput { return bytesSkipped; } try { - length = prepareForAppend(length); - Allocation writeAllocation = writeAllocationNode.allocation; - int bytesAppended = input.read(writeAllocation.data, - writeAllocation.translateOffset(lastAllocationOffset), length); + length = preAppend(length); + int bytesAppended = input.read(writeAllocationNode.allocation.data, + writeAllocationNode.translateOffset(totalBytesWritten), length); if (bytesAppended == C.RESULT_END_OF_INPUT) { if (allowEndOfInput) { return C.RESULT_END_OF_INPUT; } throw new EOFException(); } - lastAllocationOffset += bytesAppended; - totalBytesWritten += bytesAppended; + postAppend(bytesAppended); return bytesAppended; } finally { endWriteOperation(); @@ -513,13 +506,11 @@ public final class SampleQueue implements TrackOutput { return; } while (length > 0) { - int thisAppendLength = prepareForAppend(length); - Allocation writeAllocation = writeAllocationNode.allocation; - buffer.readBytes(writeAllocation.data, writeAllocation.translateOffset(lastAllocationOffset), - thisAppendLength); - lastAllocationOffset += thisAppendLength; - totalBytesWritten += thisAppendLength; - length -= thisAppendLength; + int bytesAppended = preAppend(length); + buffer.readBytes(writeAllocationNode.allocation.data, + writeAllocationNode.translateOffset(totalBytesWritten), bytesAppended); + length -= bytesAppended; + postAppend(bytesAppended); } endWriteOperation(); } @@ -567,7 +558,6 @@ public final class SampleQueue implements TrackOutput { firstAllocationNode = new AllocationNode(0, allocationLength); writeAllocationNode = firstAllocationNode; totalBytesWritten = 0; - lastAllocationOffset = allocationLength; allocator.trim(); } @@ -595,19 +585,31 @@ public final class SampleQueue implements TrackOutput { } /** - * Prepares the rolling sample buffer for an append of up to {@code length} bytes, returning the - * number of bytes that can actually be appended. + * Called before writing sample data to {@link #writeAllocationNode}. May cause + * {@link #writeAllocationNode} to be initialized. + * + * @param length The number of bytes that the caller wishes to write. + * @return The number of bytes that the caller is permitted to write, which may be less than + * {@code length}. */ - private int prepareForAppend(int length) { - if (lastAllocationOffset == allocationLength) { - lastAllocationOffset = 0; - if (writeAllocationNode.wasInitialized) { - writeAllocationNode = writeAllocationNode.next; - } + private int preAppend(int length) { + if (!writeAllocationNode.wasInitialized) { writeAllocationNode.initialize(allocator.allocate(), new AllocationNode(writeAllocationNode.endPosition, allocationLength)); } - return Math.min(length, allocationLength - lastAllocationOffset); + return Math.min(length, (int) (writeAllocationNode.endPosition - totalBytesWritten)); + } + + /** + * Called after writing sample data. May cause {@link #writeAllocationNode} to be advanced. + * + * @param length The number of bytes that were written. + */ + private void postAppend(int length) { + totalBytesWritten += length; + if (totalBytesWritten == writeAllocationNode.endPosition) { + writeAllocationNode = writeAllocationNode.next; + } } /** @@ -676,6 +678,17 @@ public final class SampleQueue implements TrackOutput { wasInitialized = true; } + /** + * Gets the offset into the {@link #allocation}'s {@link Allocation#data} that corresponds to + * the specified absolute position. + * + * @param absolutePosition The absolute position. + * @return The corresponding offset into the allocation's data. + */ + public int translateOffset(long absolutePosition) { + return (int) (absolutePosition - startPosition) + allocation.offset; + } + /** * Clears {@link #allocation}. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java index 08b42533cc..f5aa81f325 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java @@ -25,29 +25,22 @@ public final class Allocation { /** * The array containing the allocated space. The allocated space might not be at the start of the - * array, and so {@link #translateOffset(int)} method must be used when indexing into it. + * array, and so {@link #offset} must be used when indexing into it. */ public final byte[] data; - private final int offset; + /** + * The offset of the allocated space in {@link #data}. + */ + public final int offset; /** * @param data The array containing the allocated space. - * @param offset The offset of the allocated space within the array. + * @param offset The offset of the allocated space in {@code data}. */ public Allocation(byte[] data, int offset) { this.data = data; this.offset = offset; } - /** - * Translates a zero-based offset into the allocation to the corresponding {@link #data} offset. - * - * @param offset The zero-based offset to translate. - * @return The corresponding offset in {@link #data}. - */ - public int translateOffset(int offset) { - return this.offset + offset; - } - } From 8af77acb01edbcc7fe7005d44a0146e25c6695eb Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 7 Apr 2017 07:08:04 +0100 Subject: [PATCH 125/220] Adjust incorrect looking max-channel counts Issue: #2940 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159099602 --- .../extractor/flv/AudioTagPayloadReader.java | 2 +- .../exoplayer2/mediacodec/MediaCodecInfo.java | 38 ++++++++++++++++++- .../android/exoplayer2/util/MimeTypes.java | 5 ++- 3 files changed, 41 insertions(+), 4 deletions(-) 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 8e3bd08375..2f21898007 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 @@ -67,7 +67,7 @@ import java.util.Collections; hasOutputFormat = true; } else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) { String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW - : MimeTypes.AUDIO_ULAW; + : MimeTypes.AUDIO_MLAW; int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT; Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE, Format.NO_VALUE, 1, 8000, pcmEncoding, null, null, 0, null); 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 6ff5082cbd..17ef2c4456 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 @@ -274,7 +274,9 @@ public final class MediaCodecInfo { logNoSupport("channelCount.aCaps"); return false; } - if (audioCapabilities.getMaxInputChannelCount() < channelCount) { + int maxInputChannelCount = adjustMaxInputChannelCount(name, mimeType, + audioCapabilities.getMaxInputChannelCount()); + if (maxInputChannelCount < channelCount) { logNoSupport("channelCount.support, " + channelCount); return false; } @@ -291,6 +293,40 @@ public final class MediaCodecInfo { + Util.DEVICE_DEBUG_INFO + "]"); } + private static int adjustMaxInputChannelCount(String name, String mimeType, int maxChannelCount) { + if (maxChannelCount > 1 || (Util.SDK_INT >= 26 && maxChannelCount > 0)) { + // The maximum channel count looks like it's been set correctly. + return maxChannelCount; + } + if (MimeTypes.AUDIO_MPEG.equals(mimeType) + || MimeTypes.AUDIO_AMR_NB.equals(mimeType) + || MimeTypes.AUDIO_AMR_WB.equals(mimeType) + || MimeTypes.AUDIO_AAC.equals(mimeType) + || MimeTypes.AUDIO_VORBIS.equals(mimeType) + || MimeTypes.AUDIO_OPUS.equals(mimeType) + || MimeTypes.AUDIO_RAW.equals(mimeType) + || MimeTypes.AUDIO_FLAC.equals(mimeType) + || MimeTypes.AUDIO_ALAW.equals(mimeType) + || MimeTypes.AUDIO_MLAW.equals(mimeType) + || MimeTypes.AUDIO_MSGSM.equals(mimeType)) { + // Platform code should have set a default. + return maxChannelCount; + } + // The maximum channel count looks incorrect. Adjust it to an assumed default. + int assumedMaxChannelCount; + if (MimeTypes.AUDIO_AC3.equals(mimeType)) { + assumedMaxChannelCount = 6; + } else if (MimeTypes.AUDIO_E_AC3.equals(mimeType)) { + assumedMaxChannelCount = 16; + } else { + // Default to the platform limit, which is 30. + assumedMaxChannelCount = 30; + } + Log.w(TAG, "AssumedMaxChannelAdjustment: " + name + ", [" + maxChannelCount + " to " + + assumedMaxChannelCount + "]"); + return assumedMaxChannelCount; + } + private static boolean isAdaptive(CodecCapabilities capabilities) { return Util.SDK_INT >= 19 && isAdaptiveV19(capabilities); } 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 e227ea1068..db1122dbe7 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 @@ -48,7 +48,7 @@ public final class MimeTypes { public static final String AUDIO_MPEG_L2 = BASE_TYPE_AUDIO + "/mpeg-L2"; public static final String AUDIO_RAW = BASE_TYPE_AUDIO + "/raw"; public static final String AUDIO_ALAW = BASE_TYPE_AUDIO + "/g711-alaw"; - public static final String AUDIO_ULAW = BASE_TYPE_AUDIO + "/g711-mlaw"; + public static final String AUDIO_MLAW = BASE_TYPE_AUDIO + "/g711-mlaw"; public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3"; public static final String AUDIO_E_AC3 = BASE_TYPE_AUDIO + "/eac3"; public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd"; @@ -59,8 +59,9 @@ public final class MimeTypes { public static final String AUDIO_OPUS = BASE_TYPE_AUDIO + "/opus"; public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp"; public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb"; - public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/x-flac"; + public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/flac"; public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac"; + public static final String AUDIO_MSGSM = BASE_TYPE_AUDIO + "/gsm"; public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown"; public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; From b77cc7c621bc6d7fe432ed0aef041829a41120c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20Wr=C3=B3tniak?= Date: Sat, 17 Jun 2017 16:18:43 +0200 Subject: [PATCH 126/220] Introduced failing unit test for ContentDataSource --- .../core/src/androidTest/AndroidManifest.xml | 3 + .../upstream/AndroidDataSourceTest.java | 31 +++++++++ .../exoplayer2/upstream/TestDataProvider.java | 64 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceTest.java create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/TestDataProvider.java diff --git a/library/core/src/androidTest/AndroidManifest.xml b/library/core/src/androidTest/AndroidManifest.xml index 2634152c98..0a90b071d2 100644 --- a/library/core/src/androidTest/AndroidManifest.xml +++ b/library/core/src/androidTest/AndroidManifest.xml @@ -24,6 +24,9 @@ android:allowBackup="false" tools:ignore="MissingApplicationIcon,HardcodedDebugMode"> + Date: Sat, 17 Jun 2017 16:26:29 +0200 Subject: [PATCH 127/220] InputStream creation for ContentDataSource changed --- .../google/android/exoplayer2/upstream/ContentDataSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f806f47410..5d0d9a80e9 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 @@ -71,7 +71,7 @@ public final class ContentDataSource implements DataSource { try { uri = dataSpec.uri; assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r"); - inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + inputStream = assetFileDescriptor.createInputStream(); long skipped = inputStream.skip(dataSpec.position); if (skipped < dataSpec.position) { // We expect the skip to be satisfied in full. If it isn't then we're probably trying to From 50da6d870c1613c0e1bbcf67575ad30383f47593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20Wr=C3=B3tniak?= Date: Mon, 19 Jun 2017 12:46:09 +0200 Subject: [PATCH 128/220] Comments from https://github.com/google/ExoPlayer/pull/2963#discussion_r122669328 applied --- .../upstream/AndroidDataSourceConstants.java | 8 +++++ .../upstream/AndroidDataSourceTest.java | 31 ------------------- .../upstream/AssetDataSourceTest.java | 21 +++++++++++++ .../upstream/ContentDataSourceTest.java | 21 +++++++++++++ .../upstream/ContentDataSource.java | 18 ++++++----- 5 files changed, 61 insertions(+), 38 deletions(-) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceTest.java create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java new file mode 100644 index 0000000000..ad19b7a824 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java @@ -0,0 +1,8 @@ +package com.google.android.exoplayer2.upstream; + +final class AndroidDataSourceConstants { + static final long SAMPLE_MP4_BYTES = 101597; + static final String SAMPLE_MP4_PATH = "/mp4/sample.mp4"; + + private AndroidDataSourceConstants() {} +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceTest.java deleted file mode 100644 index 42dadaf379..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.google.android.exoplayer2.upstream; - -import android.content.Context; -import android.net.Uri; -import android.test.InstrumentationTestCase; - -public class AndroidDataSourceTest extends InstrumentationTestCase { - - private static final long SAMPLE_MP4_BYTES = 101597; - private static final String SAMPLE_MP4_PATH = "/mp4/sample.mp4"; - - public void testAssetDataSource() throws Exception { - final Context context = getInstrumentation().getContext(); - AssetDataSource dataSource = new AssetDataSource(context); - Uri assetUri = Uri.parse("file:///android_asset" + SAMPLE_MP4_PATH); - DataSpec dataSpec = new DataSpec(assetUri); - long sourceLengthBytes = dataSource.open(dataSpec); - - assertEquals(SAMPLE_MP4_BYTES, sourceLengthBytes); - } - - public void testContentDataSource() throws Exception { - Context context = getInstrumentation().getContext(); - ContentDataSource dataSource = new ContentDataSource(context); - Uri contentUri = Uri.parse("content://exoplayer" + SAMPLE_MP4_PATH); - DataSpec dataSpec = new DataSpec(contentUri); - long sourceLengthBytes = dataSource.open(dataSpec); - - assertEquals(SAMPLE_MP4_BYTES, sourceLengthBytes); - } -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java new file mode 100644 index 0000000000..178842bd4d --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java @@ -0,0 +1,21 @@ +package com.google.android.exoplayer2.upstream; + +import android.content.Context; +import android.net.Uri; +import android.test.InstrumentationTestCase; + +import static com.google.android.exoplayer2.upstream.AndroidDataSourceConstants.SAMPLE_MP4_BYTES; +import static com.google.android.exoplayer2.upstream.AndroidDataSourceConstants.SAMPLE_MP4_PATH; + +public class AssetDataSourceTest extends InstrumentationTestCase { + + public void testAssetDataSource() throws Exception { + final Context context = getInstrumentation().getContext(); + AssetDataSource dataSource = new AssetDataSource(context); + Uri assetUri = Uri.parse("file:///android_asset" + SAMPLE_MP4_PATH); + DataSpec dataSpec = new DataSpec(assetUri); + long sourceLengthBytes = dataSource.open(dataSpec); + + assertEquals(SAMPLE_MP4_BYTES, sourceLengthBytes); + } +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java new file mode 100644 index 0000000000..b2edeea0cc --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -0,0 +1,21 @@ +package com.google.android.exoplayer2.upstream; + +import android.content.Context; +import android.net.Uri; +import android.test.InstrumentationTestCase; + +import static com.google.android.exoplayer2.upstream.AndroidDataSourceConstants.SAMPLE_MP4_BYTES; +import static com.google.android.exoplayer2.upstream.AndroidDataSourceConstants.SAMPLE_MP4_PATH; + +public class ContentDataSourceTest extends InstrumentationTestCase { + + public void testContentDataSource() throws Exception { + Context context = getInstrumentation().getContext(); + ContentDataSource dataSource = new ContentDataSource(context); + Uri contentUri = Uri.parse("content://exoplayer" + SAMPLE_MP4_PATH); + DataSpec dataSpec = new DataSpec(contentUri); + long sourceLengthBytes = dataSource.open(dataSpec); + + assertEquals(SAMPLE_MP4_BYTES, sourceLengthBytes); + } +} 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 5d0d9a80e9..9421b7ba03 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 @@ -71,7 +71,7 @@ public final class ContentDataSource implements DataSource { try { uri = dataSpec.uri; assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r"); - inputStream = assetFileDescriptor.createInputStream(); + inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); long skipped = inputStream.skip(dataSpec.position); if (skipped < dataSpec.position) { // We expect the skip to be satisfied in full. If it isn't then we're probably trying to @@ -81,12 +81,16 @@ public final class ContentDataSource implements DataSource { if (dataSpec.length != C.LENGTH_UNSET) { bytesRemaining = dataSpec.length; } else { - bytesRemaining = inputStream.available(); - if (bytesRemaining == 0) { - // FileInputStream.available() returns 0 if the remaining length cannot be determined, or - // if it's greater than Integer.MAX_VALUE. We don't know the true length in either case, - // so treat as unbounded. - bytesRemaining = C.LENGTH_UNSET; + bytesRemaining = assetFileDescriptor.getLength(); + if (bytesRemaining == AssetFileDescriptor.UNKNOWN_LENGTH) { + // The asset must extend to the end of the file. + bytesRemaining = inputStream.available(); + if (bytesRemaining == 0) { + // FileInputStream.available() returns 0 if the remaining length cannot be determined, or + // if it's greater than Integer.MAX_VALUE. We don't know the true length in either case, + // so treat as unbounded. + bytesRemaining = C.LENGTH_UNSET; + } } } } catch (IOException e) { From 86ff19b55bd4b028b4b5215ee120b04c10b9c351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20Wr=C3=B3tniak?= Date: Mon, 19 Jun 2017 16:09:54 +0200 Subject: [PATCH 129/220] null AssetFileDescriptors support added in `ContentDataSource` --- .../upstream/AndroidDataSourceConstants.java | 8 +++++++ .../upstream/ContentDataSourceTest.java | 23 +++++++++++++++++-- .../exoplayer2/upstream/TestDataProvider.java | 4 ++++ .../upstream/ContentDataSource.java | 4 ++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java index ad19b7a824..d11202ccf2 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java @@ -1,8 +1,16 @@ package com.google.android.exoplayer2.upstream; +import android.content.ContentResolver; +import android.net.Uri; + final class AndroidDataSourceConstants { static final long SAMPLE_MP4_BYTES = 101597; static final String SAMPLE_MP4_PATH = "/mp4/sample.mp4"; + static final String TEST_DATA_PROVIDER_AUTHORITY = "exoplayer"; + static final Uri NULL_DESCRIPTOR_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(TEST_DATA_PROVIDER_AUTHORITY) + .build(); private AndroidDataSourceConstants() {} } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java index b2edeea0cc..1cd14b45e1 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -1,21 +1,40 @@ package com.google.android.exoplayer2.upstream; +import android.content.ContentResolver; import android.content.Context; import android.net.Uri; import android.test.InstrumentationTestCase; +import static com.google.android.exoplayer2.upstream.AndroidDataSourceConstants.NULL_DESCRIPTOR_URI; import static com.google.android.exoplayer2.upstream.AndroidDataSourceConstants.SAMPLE_MP4_BYTES; import static com.google.android.exoplayer2.upstream.AndroidDataSourceConstants.SAMPLE_MP4_PATH; +import static com.google.android.exoplayer2.upstream.AndroidDataSourceConstants.TEST_DATA_PROVIDER_AUTHORITY; public class ContentDataSourceTest extends InstrumentationTestCase { - public void testContentDataSource() throws Exception { + public void testValidContentDataSource() throws Exception { Context context = getInstrumentation().getContext(); ContentDataSource dataSource = new ContentDataSource(context); - Uri contentUri = Uri.parse("content://exoplayer" + SAMPLE_MP4_PATH); + Uri contentUri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(TEST_DATA_PROVIDER_AUTHORITY) + .path(SAMPLE_MP4_PATH).build(); DataSpec dataSpec = new DataSpec(contentUri); long sourceLengthBytes = dataSource.open(dataSpec); assertEquals(SAMPLE_MP4_BYTES, sourceLengthBytes); } + + public void testNullContentDataSource() throws Exception { + Context context = getInstrumentation().getContext(); + ContentDataSource dataSource = new ContentDataSource(context); + DataSpec dataSpec = new DataSpec(NULL_DESCRIPTOR_URI); + + try { + dataSource.open(dataSpec); + fail("Expected exception not thrown."); + } catch (ContentDataSource.ContentDataSourceException e) { + // Expected. + } + } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/TestDataProvider.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/TestDataProvider.java index 851e7b4b0c..f6e09a7067 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/TestDataProvider.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/TestDataProvider.java @@ -29,6 +29,10 @@ public class TestDataProvider extends ContentProvider { @Nullable @Override public AssetFileDescriptor openAssetFile(@NonNull final Uri uri, @NonNull final String mode) throws FileNotFoundException { + if (uri.equals(AndroidDataSourceConstants.NULL_DESCRIPTOR_URI)) { + return null; + } + try { Context context = getContext(); assertNotNull(context); 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 9421b7ba03..3a9be552d2 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 @@ -22,6 +22,7 @@ import android.net.Uri; import com.google.android.exoplayer2.C; import java.io.EOFException; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -71,6 +72,9 @@ public final class ContentDataSource implements DataSource { try { uri = dataSpec.uri; assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r"); + if (assetFileDescriptor == null) { + throw new FileNotFoundException("Could not open file descriptor for: " + uri); + } inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); long skipped = inputStream.skip(dataSpec.position); if (skipped < dataSpec.position) { From ed27017b0ca8061559d7ebcee98154c4e5c66c3e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 15 Jun 2017 08:06:31 -0700 Subject: [PATCH 130/220] Update MIME type in FLAC test data ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159104188 --- library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump | 2 +- library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump | 2 +- library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump | 2 +- library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump | 2 +- .../core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump | 2 +- .../src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump | 2 +- .../src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump | 2 +- .../src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump | 2 +- .../src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump | 2 +- .../assets/ogg/bear_flac_noseektable.ogg.unklen.dump | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump index 16816917b7..5ba8cc29ae 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump index fec523f971..f698fd28cf 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump index a4a60989ed..8d803d0bac 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump index a77575bb0c..09f6267270 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump index 16816917b7..5ba8cc29ae 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump index 7be7d02493..73e537f8c8 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump index 34f19c6bce..3b7dc3fd1e 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump index 68484d2cf4..b6a6741fcc 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump index 8b2e7858b0..738002f7ef 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump index 8d398efdb8..a237fd0dfc 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 From 023c9d56a914fe4c298f7abb553a0b5c060abaa9 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 15 Jun 2017 08:53:47 -0700 Subject: [PATCH 131/220] Simplify timeline test stub class. Use an actual class for the stub media source instead of an anomymous class. Allows to call assertReleased() on that class. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159109143 --- .../android/exoplayer2/TimelineTest.java | 61 ++++++++++++------- .../source/ClippingMediaSourceTest.java | 3 +- .../source/ConcatenatingMediaSourceTest.java | 3 +- .../source/LoopingMediaSourceTest.java | 9 +-- 4 files changed, 47 insertions(+), 29 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java index 807297910d..4c5439a0dc 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java @@ -73,37 +73,52 @@ public class TimelineTest extends TestCase { } /** - * Returns a stub {@link MediaSource} with the specified {@link Timeline} in its source info. + * Stub media source which returns a provided timeline as source info and keeps track if it is + * prepared and released. */ - public static MediaSource stubMediaSourceSourceWithTimeline(final Timeline timeline) { - return new MediaSource() { - @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - listener.onSourceInfoRefreshed(timeline, null); - } + public static class StubMediaSource implements MediaSource { + private final Timeline timeline; - @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - } + private boolean isPrepared; + private volatile boolean isReleased; - @Override - public MediaPeriod createPeriod(int index, Allocator allocator) { - return null; - } + public StubMediaSource(Timeline timeline) { + this.timeline = timeline; + } - @Override - public void releasePeriod(MediaPeriod mediaPeriod) { - } + @Override + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + assertFalse(isPrepared); + listener.onSourceInfoRefreshed(timeline, null); + isPrepared = true; + } - @Override - public void releaseSource() { - } - }; + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + } + + @Override + public MediaPeriod createPeriod(int index, Allocator allocator) { + return null; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + } + + @Override + public void releaseSource() { + assertTrue(isPrepared); + isReleased = true; + } + + public void assertReleased() { + assertTrue(isReleased); + } } /** - * Works in conjunction with {@code stubMediaSourceSourceWithTimeline} to extract the Timeline - * from a media source. + * Extracts the timeline from a media source. */ public static Timeline extractTimelineFromMediaSource(MediaSource mediaSource) { class TimelineListener implements Listener { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index f570272bef..145f6fc179 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.TimelineTest; import com.google.android.exoplayer2.TimelineTest.FakeTimeline; +import com.google.android.exoplayer2.TimelineTest.StubMediaSource; import com.google.android.exoplayer2.TimelineTest.TimelineVerifier; /** @@ -119,7 +120,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline. */ private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { - MediaSource mediaSource = TimelineTest.stubMediaSourceSourceWithTimeline(timeline); + MediaSource mediaSource = new StubMediaSource(timeline); return TimelineTest.extractTimelineFromMediaSource( new ClippingMediaSource(mediaSource, startMs, endMs)); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 08d2c1cda7..a8a80517f2 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.TimelineTest; import com.google.android.exoplayer2.TimelineTest.FakeTimeline; +import com.google.android.exoplayer2.TimelineTest.StubMediaSource; import com.google.android.exoplayer2.TimelineTest.TimelineVerifier; import junit.framework.TestCase; @@ -101,7 +102,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { Timeline... timelines) { MediaSource[] mediaSources = new MediaSource[timelines.length]; for (int i = 0; i < timelines.length; i++) { - mediaSources[i] = TimelineTest.stubMediaSourceSourceWithTimeline(timelines[i]); + mediaSources[i] = new StubMediaSource(timelines[i]); } return TimelineTest.extractTimelineFromMediaSource( new ConcatenatingMediaSource(isRepeatOneAtomic, mediaSources)); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index 7b4a449764..dcb0c3d3bf 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.TimelineTest; import com.google.android.exoplayer2.TimelineTest.FakeTimeline; +import com.google.android.exoplayer2.TimelineTest.StubMediaSource; import com.google.android.exoplayer2.TimelineTest.TimelineVerifier; import junit.framework.TestCase; @@ -33,9 +34,9 @@ public class LoopingMediaSourceTest extends TestCase { public LoopingMediaSourceTest() { multiWindowTimeline = TimelineTest.extractTimelineFromMediaSource( new ConcatenatingMediaSource( - TimelineTest.stubMediaSourceSourceWithTimeline(new FakeTimeline(1, 111)), - TimelineTest.stubMediaSourceSourceWithTimeline(new FakeTimeline(1, 222)), - TimelineTest.stubMediaSourceSourceWithTimeline(new FakeTimeline(1, 333)))); + new StubMediaSource(new FakeTimeline(1, 111)), + new StubMediaSource(new FakeTimeline(1, 222)), + new StubMediaSource(new FakeTimeline(1, 333)))); } public void testSingleLoop() { @@ -83,7 +84,7 @@ public class LoopingMediaSourceTest extends TestCase { * the looping timeline. */ private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) { - MediaSource mediaSource = TimelineTest.stubMediaSourceSourceWithTimeline(timeline); + MediaSource mediaSource = new StubMediaSource(timeline); return TimelineTest.extractTimelineFromMediaSource( new LoopingMediaSource(mediaSource, loopCount)); } From c7948f2f7a9b9bee91b116fbcc61e727443453cf Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 7 May 2017 05:37:26 +0100 Subject: [PATCH 132/220] TTML: Ignore regions that don't declare origin and extent Issue: #2953 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159218386 --- .../exoplayer2/text/ttml/TtmlDecoderTest.java | 10 ++++++--- .../exoplayer2/text/ttml/TtmlDecoder.java | 22 +++++++++++++------ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java index 496e3f87de..492cf036b4 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java @@ -179,9 +179,13 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { assertEquals(1, output.size()); ttmlCue = output.get(0); assertEquals("dolor", ttmlCue.text.toString()); - assertEquals(10f / 100f, ttmlCue.position); - assertEquals(80f / 100f, ttmlCue.line); - assertEquals(1f, ttmlCue.size); + assertEquals(Cue.DIMEN_UNSET, ttmlCue.position); + assertEquals(Cue.DIMEN_UNSET, ttmlCue.line); + assertEquals(Cue.DIMEN_UNSET, ttmlCue.size); + // TODO: Should be as below, once https://github.com/google/ExoPlayer/issues/2953 is fixed. + // assertEquals(10f / 100f, ttmlCue.position); + // assertEquals(80f / 100f, ttmlCue.line); + // assertEquals(1f, ttmlCue.size); output = subtitle.getCues(21000000); assertEquals(1, output.size()); 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 0012ce2c22..e438aa1837 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 @@ -222,9 +222,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { /** * Parses a region declaration. *

        - * If the region defines an origin and/or extent, it is required that they're defined as - * percentages of the viewport. Region declarations that define origin and/or extent in other - * formats are unsupported, and null is returned. + * If the region defines an origin and extent, it is required that they're defined as percentages + * of the viewport. Region declarations that define origin and extent in other formats are + * unsupported, and null is returned. */ private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser) { String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID); @@ -250,9 +250,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { return null; } } else { + Log.w(TAG, "Ignoring region without an origin"); + return null; + // TODO: Should default to top left as below in this case, but need to fix + // https://github.com/google/ExoPlayer/issues/2953 first. // Origin is omitted. Default to top left. - position = 0; - line = 0; + // position = 0; + // line = 0; } float width; @@ -273,9 +277,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { return null; } } else { + Log.w(TAG, "Ignoring region without an extent"); + return null; + // TODO: Should default to extent of parent as below in this case, but need to fix + // https://github.com/google/ExoPlayer/issues/2953 first. // Extent is omitted. Default to extent of parent. - width = 1; - height = 1; + // width = 1; + // height = 1; } @Cue.AnchorType int lineAnchor = Cue.ANCHOR_TYPE_START; From 56205893e5f392531baa9bc1d33a56858193a36e Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 16 Jun 2017 06:41:19 -0700 Subject: [PATCH 133/220] Remove initial discontinuity from ClippingMediaSource (Fixing GitHub issue #2923) Cuurently, ClippingMediaSource issues an initial discontinuity. This causes the renderers to be disabled and re-enabled when this media source is used in a sequence with other sources (or in a loop). This change disables the use of an initial discontinuity for audio-only media under the assumption that audio streams have random access capabilities. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159221963 --- .../source/ClippingMediaPeriod.java | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) 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 a7690d8d74..9b580c832f 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 @@ -16,10 +16,12 @@ package com.google.android.exoplayer2.source; 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.trackselection.TrackSelection; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; /** @@ -49,10 +51,29 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb * @param mediaPeriod The media period to clip. */ public ClippingMediaPeriod(MediaPeriod mediaPeriod) { + this(mediaPeriod, true); + } + + /** + * Creates a new clipping media period that provides a clipped view of the specified + * {@link MediaPeriod}'s sample streams. + *

        + * The clipping start/end positions must be specified by calling {@link #setClipping(long, long)} + * on the playback thread before preparation completes. + *

        + * If the start point is guaranteed to be a key frame, pass {@code false} to {@code + * enableInitialPositionDiscontinuity} to suppress an initial discontinuity when the period is + * first read from. + * + * @param mediaPeriod The media period to clip. + * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled. + */ + public ClippingMediaPeriod(MediaPeriod mediaPeriod, boolean enableInitialDiscontinuity) { this.mediaPeriod = mediaPeriod; startUs = C.TIME_UNSET; endUs = C.TIME_UNSET; sampleStreams = new ClippingSampleStream[0]; + pendingInitialDiscontinuity = enableInitialDiscontinuity; } /** @@ -94,6 +115,9 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags, internalStreams, streamResetFlags, positionUs + startUs); + if (pendingInitialDiscontinuity) { + pendingInitialDiscontinuity = startUs != 0 && shouldKeepInitialDiscontinuity(selections); + } Assertions.checkState(enablePositionUs == positionUs + startUs || (enablePositionUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs))); @@ -179,6 +203,15 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public void onPrepared(MediaPeriod mediaPeriod) { Assertions.checkState(startUs != C.TIME_UNSET && endUs != C.TIME_UNSET); + callback.onPrepared(this); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + callback.onContinueLoadingRequested(this); + } + + private static boolean shouldKeepInitialDiscontinuity(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, @@ -186,13 +219,17 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb // negative timestamp, its offset timestamp can jump backwards compared to the last timestamp // read in the previous period. Renderer implementations may not allow this, so we signal a // discontinuity which resets the renderers before they read the clipping sample stream. - pendingInitialDiscontinuity = startUs != 0; - callback.onPrepared(this); - } - - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - callback.onContinueLoadingRequested(this); + // However, for audio-only track selections we assume to have random access seek behaviour and + // do not need an initial discontinuity to reset the renderer. + for (TrackSelection trackSelection : selections) { + if (trackSelection != null) { + Format selectedFormat = trackSelection.getSelectedFormat(); + if (!MimeTypes.isAudio(selectedFormat.sampleMimeType)) { + return true; + } + } + } + return false; } /** From d9ea8f71430bed129917b5eb9554a8f1debf8018 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 16 Jun 2017 07:51:58 -0700 Subject: [PATCH 134/220] Allow disabling the initial discontinuity on ClippingMediaSource ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159227077 --- .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 2 +- .../source/ClippingMediaPeriod.java | 13 ---------- .../source/ClippingMediaSource.java | 26 +++++++++++++++++-- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 00bd81d754..563685e6dc 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -181,7 +181,7 @@ public final class ImaAdsMediaSource implements MediaSource { long startUs = timeline.getContentStartTimeUs(index); long endUs = timeline.getContentEndTimeUs(index); MediaPeriod contentMediaPeriod = contentMediaSource.createPeriod(0, allocator); - ClippingMediaPeriod clippingPeriod = new ClippingMediaPeriod(contentMediaPeriod); + ClippingMediaPeriod clippingPeriod = new ClippingMediaPeriod(contentMediaPeriod, true); clippingPeriod.setClipping(startUs, endUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE : endUs); mediaSourceByMediaPeriod.put(contentMediaPeriod, contentMediaSource); return clippingPeriod; 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 9b580c832f..12f58d9a21 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 @@ -41,19 +41,6 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb private ClippingSampleStream[] sampleStreams; private boolean pendingInitialDiscontinuity; - /** - * Creates a new clipping media period that provides a clipped view of the specified - * {@link MediaPeriod}'s sample streams. - *

        - * The clipping start/end positions must be specified by calling {@link #setClipping(long, long)} - * on the playback thread before preparation completes. - * - * @param mediaPeriod The media period to clip. - */ - public ClippingMediaPeriod(MediaPeriod mediaPeriod) { - this(mediaPeriod, true); - } - /** * Creates a new clipping media period that provides a clipped view of the specified * {@link MediaPeriod}'s sample streams. 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 c1ae082203..60ee198ed6 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 @@ -34,6 +34,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste private final MediaSource mediaSource; private final long startUs; private final long endUs; + private final boolean enableInitialDiscontinuity; private final ArrayList mediaPeriods; private MediaSource.Listener sourceListener; @@ -50,10 +51,31 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste * from the specified start point up to the end of the source. */ public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) { + this(mediaSource, startPositionUs, endPositionUs, true); + } + + /** + * Creates a new clipping source that wraps the specified source. + *

        + * If the start point is guaranteed to be a key frame, pass {@code false} to + * {@code enableInitialPositionDiscontinuity} to suppress an initial discontinuity when a period + * is first read from. + * + * @param mediaSource The single-period, non-dynamic source to wrap. + * @param startPositionUs The start position within {@code mediaSource}'s timeline at which to + * start providing samples, in microseconds. + * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop + * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples + * from the specified start point up to the end of the source. + * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled. + */ + public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs, + boolean enableInitialDiscontinuity) { Assertions.checkArgument(startPositionUs >= 0); this.mediaSource = Assertions.checkNotNull(mediaSource); startUs = startPositionUs; endUs = endPositionUs; + this.enableInitialDiscontinuity = enableInitialDiscontinuity; mediaPeriods = new ArrayList<>(); } @@ -70,8 +92,8 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste @Override public MediaPeriod createPeriod(int index, Allocator allocator) { - ClippingMediaPeriod mediaPeriod = - new ClippingMediaPeriod(mediaSource.createPeriod(index, allocator)); + ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod( + mediaSource.createPeriod(index, allocator), enableInitialDiscontinuity); mediaPeriods.add(mediaPeriod); mediaPeriod.setClipping(clippingTimeline.startUs, clippingTimeline.endUs); return mediaPeriod; From 56ff2ef598d1bcc2e5f78d2fe8e27b20f731b386 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 19 Jun 2017 05:37:25 -0700 Subject: [PATCH 135/220] Add getPlaybackLooper() to ExoPlayer v2. A few components in ExoPlayer requires playback looper to operate (such as: DrmSessionManager#acquireSession), so this CL add back getPlaybackLooper() to facilitate such cases. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159416012 --- .../main/java/com/google/android/exoplayer2/ExoPlayer.java | 7 +++++++ .../java/com/google/android/exoplayer2/ExoPlayerImpl.java | 5 +++++ .../google/android/exoplayer2/ExoPlayerImplInternal.java | 5 +++++ .../com/google/android/exoplayer2/SimpleExoPlayer.java | 5 +++++ 4 files changed, 22 insertions(+) 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 20ad3c0d22..4ef1caf8c7 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 @@ -283,6 +283,13 @@ public interface ExoPlayer { */ int REPEAT_MODE_ALL = 2; + /** + * Gets the {@link Looper} associated with the playback thread. + * + * @return The {@link Looper} associated with the playback thread. + */ + Looper getPlaybackLooper(); + /** * Register a listener to receive events from the player. The listener's methods will be called on * the thread that was used to construct the player. However, if the thread used to construct the 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 1350c13943..680cbe71ff 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 @@ -106,6 +106,11 @@ import java.util.concurrent.CopyOnWriteArraySet; repeatMode, eventHandler, playbackInfo, this); } + @Override + public Looper getPlaybackLooper() { + return internalPlayer.getPlaybackLooper(); + } + @Override public void addListener(EventListener listener) { listeners.add(listener); 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 b93b31bdaa..bcfcc4451b 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2; import android.os.Handler; import android.os.HandlerThread; +import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.SystemClock; @@ -270,6 +271,10 @@ import java.io.IOException; internalPlaybackThread.quit(); } + public Looper getPlaybackLooper() { + return internalPlaybackThread.getLooper(); + } + // MediaSource.Listener implementation. @Override 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 a8c1d1d9f0..9bf98ff0dc 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 @@ -480,6 +480,11 @@ public class SimpleExoPlayer implements ExoPlayer { // ExoPlayer implementation + @Override + public Looper getPlaybackLooper() { + return player.getPlaybackLooper(); + } + @Override public void addListener(EventListener listener) { player.addListener(listener); From de4ff4c5ec71698fb4d2ff9ce4572929d70d477a Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 19 Jun 2017 06:23:22 -0700 Subject: [PATCH 136/220] Extend HostActivity for reuse and silent timeout. Added option to fail on timeout. Also reset internals in all cases such that the activity can be used more than once. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159419176 --- .../exoplayer2/testutil/HostActivity.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) 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 ecbe00b487..831344aa8b 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 @@ -101,6 +101,17 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba * is exceeded then the test will fail. */ public void runTest(final HostedTest hostedTest, long timeoutMs) { + runTest(hostedTest, timeoutMs, true); + } + + /** + * Executes a {@link HostedTest} inside the host. + * + * @param hostedTest The test to execute. + * @param timeoutMs The number of milliseconds to wait for the test to finish. + * @param failOnTimeout Whether the test fails when the timeout is exceeded. + */ + public void runTest(final HostedTest hostedTest, long timeoutMs, boolean failOnTimeout) { Assertions.checkArgument(timeoutMs > 0); Assertions.checkState(Thread.currentThread() != getMainLooper().getThread()); @@ -131,7 +142,11 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba } else { String message = "Test timed out after " + timeoutMs + " ms."; Log.e(TAG, message); - fail(message); + if (failOnTimeout) { + fail(message); + } + maybeStopHostedTest(); + hostedTestStoppedCondition.block(); } } From 86f06faa8df1e8cbbfb0b298ed4fa9d98bae9703 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 19 Jun 2017 06:48:18 -0700 Subject: [PATCH 137/220] Move clearing of joining deadline back to onStopped ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159421000 --- .../google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 2 +- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 2 +- 2 files changed, 2 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 e661aae892..d0d3b99973 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 @@ -478,11 +478,11 @@ public final class LibvpxVideoRenderer extends BaseRenderer { protected void onStarted() { droppedFrames = 0; droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); - joiningDeadlineMs = C.TIME_UNSET; } @Override protected void onStopped() { + joiningDeadlineMs = C.TIME_UNSET; maybeNotifyDroppedFrames(); } 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 75e10f05ff..5ef865b817 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 @@ -290,11 +290,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { super.onStarted(); droppedFrames = 0; droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); - joiningDeadlineMs = C.TIME_UNSET; } @Override protected void onStopped() { + joiningDeadlineMs = C.TIME_UNSET; maybeNotifyDroppedFrames(); super.onStopped(); } From 2c09f8e0e315d06600710e355734485e1cd971c1 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 19 Jun 2017 07:59:43 -0700 Subject: [PATCH 138/220] Decouple start and read indices in SampleMetadataQueue ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159426366 --- .../source/SampleMetadataQueue.java | 140 ++++++++++-------- .../exoplayer2/source/SampleQueue.java | 20 ++- 2 files changed, 90 insertions(+), 70 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 a114c6eae3..f52d84ef69 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 @@ -35,7 +35,6 @@ import com.google.android.exoplayer2.util.Util; public int size; public long offset; - public long nextOffset; public CryptoData cryptoData; } @@ -54,8 +53,9 @@ import com.google.android.exoplayer2.util.Util; private int length; private int absoluteStartIndex; private int relativeStartIndex; - private int relativeEndIndex; + private int readPosition; + private long nextOffset; private long largestDequeuedTimestampUs; private long largestQueuedTimestampUs; private boolean upstreamKeyframeRequired; @@ -79,10 +79,10 @@ import com.google.android.exoplayer2.util.Util; } public void clearSampleData() { + length = 0; absoluteStartIndex = 0; relativeStartIndex = 0; - relativeEndIndex = 0; - length = 0; + readPosition = 0; upstreamKeyframeRequired = true; } @@ -108,8 +108,9 @@ import com.google.android.exoplayer2.util.Util; */ public long discardUpstreamSamples(int discardFromIndex) { int discardCount = getWriteIndex() - discardFromIndex; - Assertions.checkArgument(0 <= discardCount && discardCount <= length); + Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition)); + int relativeEndIndex = (relativeStartIndex + length) % capacity; if (discardCount == 0) { if (absoluteStartIndex == 0 && length == 0) { // Nothing has been written to the queue. @@ -144,7 +145,7 @@ import com.google.android.exoplayer2.util.Util; * Returns the current absolute read index. */ public int getReadIndex() { - return absoluteStartIndex; + return absoluteStartIndex + readPosition; } /** @@ -152,14 +153,15 @@ import com.google.android.exoplayer2.util.Util; * {@link #hasNextSample()} is {@code false}. */ public int peekSourceId() { - return hasNextSample() ? sourceIds[relativeStartIndex] : upstreamSourceId; + int relativeReadIndex = (relativeStartIndex + readPosition) % capacity; + return hasNextSample() ? sourceIds[relativeReadIndex] : upstreamSourceId; } /** * Returns whether a sample is available to be read. */ public synchronized boolean hasNextSample() { - return length != 0; + return readPosition != length; } /** @@ -184,6 +186,13 @@ import com.google.android.exoplayer2.util.Util; return Math.max(largestDequeuedTimestampUs, largestQueuedTimestampUs); } + /** + * Rewinds the read position to the first sample retained in the queue. + */ + public synchronized void rewind() { + readPosition = 0; + } + /** * Attempts to read from the queue. * @@ -222,8 +231,9 @@ import com.google.android.exoplayer2.util.Util; } } - if (formatRequired || formats[relativeStartIndex] != downstreamFormat) { - formatHolder.format = formats[relativeStartIndex]; + int relativeReadIndex = (relativeStartIndex + readPosition) % capacity; + if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { + formatHolder.format = formats[relativeReadIndex]; return C.RESULT_FORMAT_READ; } @@ -231,62 +241,37 @@ import com.google.android.exoplayer2.util.Util; return C.RESULT_NOTHING_READ; } - buffer.timeUs = timesUs[relativeStartIndex]; - buffer.setFlags(flags[relativeStartIndex]); - extrasHolder.size = sizes[relativeStartIndex]; - extrasHolder.offset = offsets[relativeStartIndex]; - extrasHolder.cryptoData = cryptoDatas[relativeStartIndex]; + buffer.timeUs = timesUs[relativeReadIndex]; + buffer.setFlags(flags[relativeReadIndex]); + extrasHolder.size = sizes[relativeReadIndex]; + extrasHolder.offset = offsets[relativeReadIndex]; + extrasHolder.cryptoData = cryptoDatas[relativeReadIndex]; largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, buffer.timeUs); - length--; - relativeStartIndex++; - absoluteStartIndex++; - if (relativeStartIndex == capacity) { - // Wrap around. - relativeStartIndex = 0; - } + readPosition++; + nextOffset = extrasHolder.offset + extrasHolder.size; - extrasHolder.nextOffset = length > 0 ? offsets[relativeStartIndex] - : extrasHolder.offset + extrasHolder.size; return C.RESULT_BUFFER_READ; } /** - * Skips all samples in the buffer. - * - * @return The offset up to which data should be dropped, or {@link C#POSITION_UNSET} if no - * dropping of data is required. - */ - public synchronized long skipAll() { - if (!hasNextSample()) { - return C.POSITION_UNSET; - } - - int lastSampleIndex = (relativeStartIndex + length - 1) % capacity; - relativeStartIndex = (relativeStartIndex + length) % capacity; - absoluteStartIndex += length; - length = 0; - return offsets[lastSampleIndex] + sizes[lastSampleIndex]; - } - - /** - * Attempts to locate the keyframe before or at the specified time. If + * Attempts to advance the read position to the keyframe before or at the specified time. If * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs} * falls within the buffer. * * @param timeUs The seek time. * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end * of the buffer. - * @return The offset of the keyframe's data if the keyframe was present. - * {@link C#POSITION_UNSET} otherwise. + * @return Whether the read position was advanced to the keyframe before or at the specified time. */ - public synchronized long skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) { - if (!hasNextSample() || timeUs < timesUs[relativeStartIndex]) { - return C.POSITION_UNSET; + public synchronized boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) { + int relativeReadIndex = (relativeStartIndex + readPosition) % capacity; + if (!hasNextSample() || timeUs < timesUs[relativeReadIndex]) { + return false; } if (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer) { - return C.POSITION_UNSET; + return false; } // This could be optimized to use a binary search, however in practice callers to this method @@ -294,7 +279,8 @@ import com.google.android.exoplayer2.util.Util; // a binary search would yield any real benefit. int sampleCount = 0; int sampleCountToKeyframe = -1; - int searchIndex = relativeStartIndex; + int searchIndex = relativeReadIndex; + int relativeEndIndex = (relativeStartIndex + length) % capacity; while (searchIndex != relativeEndIndex) { if (timesUs[searchIndex] > timeUs) { // We've gone too far. @@ -308,13 +294,44 @@ import com.google.android.exoplayer2.util.Util; } if (sampleCountToKeyframe == -1) { - return C.POSITION_UNSET; + return false; } - relativeStartIndex = (relativeStartIndex + sampleCountToKeyframe) % capacity; - absoluteStartIndex += sampleCountToKeyframe; - length -= sampleCountToKeyframe; - return offsets[relativeStartIndex]; + readPosition += sampleCountToKeyframe; + nextOffset = offsets[(relativeStartIndex + readPosition) % capacity]; + return true; + } + + /** + * Skips all samples in the buffer. + */ + public synchronized void skipAll() { + if (!hasNextSample()) { + return; + } + readPosition = length; + int relativeLastSampleIndex = (relativeStartIndex + readPosition - 1) % capacity; + nextOffset = offsets[relativeLastSampleIndex] + sizes[relativeLastSampleIndex]; + } + + /** + * Discards all samples in the buffer prior to the read position. + * + * @return The offset up to which data should be dropped, or {@link C#POSITION_UNSET} if no + * dropping of data is required. + */ + public synchronized long discardToRead() { + if (readPosition == 0) { + return C.POSITION_UNSET; + } + length -= readPosition; + absoluteStartIndex += readPosition; + relativeStartIndex += readPosition; + if (relativeStartIndex >= capacity) { + relativeStartIndex -= capacity; + } + readPosition = 0; + return length == 0 ? nextOffset : offsets[relativeStartIndex]; } // Called by the loading thread. @@ -344,6 +361,8 @@ import com.google.android.exoplayer2.util.Util; } Assertions.checkState(!upstreamFormatRequired); commitSampleTimestamp(timeUs); + + int relativeEndIndex = (relativeStartIndex + length) % capacity; timesUs[relativeEndIndex] = timeUs; offsets[relativeEndIndex] = offset; sizes[relativeEndIndex] = size; @@ -351,7 +370,7 @@ import com.google.android.exoplayer2.util.Util; cryptoDatas[relativeEndIndex] = cryptoData; formats[relativeEndIndex] = upstreamFormat; sourceIds[relativeEndIndex] = upstreamSourceId; - // Increment the write index. + length++; if (length == capacity) { // Increase the capacity. @@ -387,15 +406,8 @@ import com.google.android.exoplayer2.util.Util; formats = newFormats; sourceIds = newSourceIds; relativeStartIndex = 0; - relativeEndIndex = capacity; length = capacity; capacity = newCapacity; - } else { - relativeEndIndex++; - if (relativeEndIndex == capacity) { - // Wrap around. - relativeEndIndex = 0; - } } } @@ -415,7 +427,7 @@ import com.google.android.exoplayer2.util.Util; return false; } int retainCount = length; - while (retainCount > 0 + while (retainCount > readPosition && timesUs[(relativeStartIndex + retainCount - 1) % capacity] >= timeUs) { retainCount--; } 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 2551307004..1a2e65ceaa 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 @@ -227,7 +227,8 @@ public final class SampleQueue implements TrackOutput { * Skips all samples currently in the buffer. */ public void skipAll() { - long nextOffset = metadataQueue.skipAll(); + metadataQueue.skipAll(); + long nextOffset = metadataQueue.discardToRead(); if (nextOffset != C.POSITION_UNSET) { dropDownstreamTo(nextOffset); } @@ -245,12 +246,16 @@ public final class SampleQueue implements TrackOutput { * @return Whether the skip was successful. */ public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) { - long nextOffset = metadataQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer); - if (nextOffset == C.POSITION_UNSET) { + boolean success = metadataQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer); + if (success) { + long nextOffset = metadataQueue.discardToRead(); + if (nextOffset != C.POSITION_UNSET) { + dropDownstreamTo(nextOffset); + } + return true; + } else { return false; } - dropDownstreamTo(nextOffset); - return true; } /** @@ -290,7 +295,10 @@ public final class SampleQueue implements TrackOutput { buffer.ensureSpaceForWrite(extrasHolder.size); readData(extrasHolder.offset, buffer.data, extrasHolder.size); // Advance the read head. - dropDownstreamTo(extrasHolder.nextOffset); + long nextOffset = metadataQueue.discardToRead(); + if (nextOffset != C.POSITION_UNSET) { + dropDownstreamTo(nextOffset); + } } return C.RESULT_BUFFER_READ; case C.RESULT_NOTHING_READ: From 317a9944997d3bfaccb1deae1ac50acb2b8bf1e4 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 20 Jun 2017 02:52:55 -0700 Subject: [PATCH 139/220] Add type to DrmInitData.SchemeData At the moment, only CENC-defined scheme types are known values. This will allow having more information about the encryption scheme through the format, which in turn will allow more informed decisions on format support. Issue:#1661 Issue:#1989 Issue:#2089 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159538907 --- .../google/android/exoplayer2/FormatTest.java | 4 +- .../exoplayer2/drm/DrmInitDataTest.java | 20 +++---- .../drm/OfflineLicenseHelperTest.java | 2 +- .../android/exoplayer2/drm/DrmInitData.java | 60 +++++++++++++++++-- .../extractor/mkv/MatroskaExtractor.java | 2 +- .../exoplayer2/extractor/mp4/AtomParsers.java | 51 +++++++++++----- .../extractor/mp4/FragmentedMp4Extractor.java | 28 +++++---- .../exoplayer2/extractor/mp4/Track.java | 22 +++++-- .../extractor/mp4/TrackEncryptionBox.java | 19 ++++-- .../exoplayer2/source/dash/DashUtilTest.java | 2 +- .../dash/manifest/DashManifestParser.java | 5 +- .../source/smoothstreaming/SsMediaPeriod.java | 2 +- .../manifest/SsManifestParser.java | 2 +- 13 files changed, 158 insertions(+), 61 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java index a47a3fb12d..316fb11e9a 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java @@ -53,9 +53,9 @@ public final class FormatTest extends TestCase { } public void testParcelable() { - DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, VIDEO_MP4, + DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, "cenc", VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */)); - DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, VIDEO_WEBM, + DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, null, VIDEO_WEBM, TestUtil.buildTestData(128, 1 /* data seed */)); DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2); byte[] projectionData = new byte[] {1, 2, 3}; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java index df2e8756a5..b7f1cd1ed6 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java @@ -31,16 +31,16 @@ import junit.framework.TestCase; */ public class DrmInitDataTest extends TestCase { - private static final SchemeData DATA_1 = - new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */)); - private static final SchemeData DATA_2 = - new SchemeData(PLAYREADY_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 2 /* data seed */)); - private static final SchemeData DATA_1B = - new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */)); - private static final SchemeData DATA_2B = - new SchemeData(PLAYREADY_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 2 /* data seed */)); - private static final SchemeData DATA_UNIVERSAL = - new SchemeData(C.UUID_NIL, VIDEO_MP4, TestUtil.buildTestData(128, 3 /* data seed */)); + private static final SchemeData DATA_1 = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4, + TestUtil.buildTestData(128, 1 /* data seed */)); + private static final SchemeData DATA_2 = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4, + TestUtil.buildTestData(128, 2 /* data seed */)); + private static final SchemeData DATA_1B = new SchemeData(WIDEVINE_UUID, "cens", VIDEO_MP4, + TestUtil.buildTestData(128, 1 /* data seed */)); + private static final SchemeData DATA_2B = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4, + TestUtil.buildTestData(128, 2 /* data seed */)); + private static final SchemeData DATA_UNIVERSAL = new SchemeData(C.UUID_NIL, null, VIDEO_MP4, + TestUtil.buildTestData(128, 3 /* data seed */)); public void testParcelable() { DrmInitData drmInitDataToParcel = new DrmInitData(DATA_1, DATA_2); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index afd690762b..9f5b067b5e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -154,7 +154,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { } private static DrmInitData newDrmInitData() { - return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType", + return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "cenc", "mimeType", new byte[] {1, 4, 7, 0, 3, 6})); } 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 5126628dd9..9fa6547a00 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.drm; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.util.Assertions; @@ -102,6 +103,33 @@ public final class DrmInitData implements Comparator, Parcelable { return schemeDatas[index]; } + /** + * Returns a copy of the {@link DrmInitData} instance whose {@link SchemeData}s have been updated + * to have the specified scheme type. + * + * @param schemeType A protection scheme type. May be null. + * @return A copy of the {@link DrmInitData} instance whose {@link SchemeData}s have been updated + * to have the specified scheme type. + */ + public DrmInitData copyWithSchemeType(@Nullable String schemeType) { + boolean isCopyRequired = false; + for (SchemeData schemeData : schemeDatas) { + if (!Util.areEqual(schemeData.type, schemeType)) { + isCopyRequired = true; + break; + } + } + if (isCopyRequired) { + SchemeData[] schemeDatas = new SchemeData[this.schemeDatas.length]; + for (int i = 0; i < schemeDatas.length; i++) { + schemeDatas[i] = this.schemeDatas[i].copyWithSchemeType(schemeType); + } + return new DrmInitData(schemeDatas); + } else { + return this; + } + } + @Override public int hashCode() { if (hashCode == 0) { @@ -167,6 +195,10 @@ public final class DrmInitData implements Comparator, Parcelable { * applies to all schemes). */ private final UUID uuid; + /** + * The protection scheme type, or null if not applicable or unknown. + */ + @Nullable public final String type; /** * The mimeType of {@link #data}. */ @@ -183,22 +215,26 @@ public final class DrmInitData implements Comparator, Parcelable { /** * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is * universal (i.e. applies to all schemes). + * @param type The type of the protection scheme, or null if not applicable or unknown. * @param mimeType The mimeType of the initialization data. * @param data The initialization data. */ - public SchemeData(UUID uuid, String mimeType, byte[] data) { - this(uuid, mimeType, data, false); + public SchemeData(UUID uuid, @Nullable String type, String mimeType, byte[] data) { + this(uuid, type, mimeType, data, false); } /** * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is * universal (i.e. applies to all schemes). + * @param type The type of the protection scheme, or null if not applicable or unknown. * @param mimeType The mimeType of the initialization data. * @param data The initialization data. * @param requiresSecureDecryption Whether secure decryption is required. */ - public SchemeData(UUID uuid, String mimeType, byte[] data, boolean requiresSecureDecryption) { + public SchemeData(UUID uuid, @Nullable String type, String mimeType, byte[] data, + boolean requiresSecureDecryption) { this.uuid = Assertions.checkNotNull(uuid); + this.type = type; this.mimeType = Assertions.checkNotNull(mimeType); this.data = Assertions.checkNotNull(data); this.requiresSecureDecryption = requiresSecureDecryption; @@ -206,6 +242,7 @@ public final class DrmInitData implements Comparator, Parcelable { /* package */ SchemeData(Parcel in) { uuid = new UUID(in.readLong(), in.readLong()); + type = in.readString(); mimeType = in.readString(); data = in.createByteArray(); requiresSecureDecryption = in.readByte() != 0; @@ -221,6 +258,19 @@ public final class DrmInitData implements Comparator, Parcelable { return C.UUID_NIL.equals(uuid) || schemeUuid.equals(uuid); } + /** + * Returns a copy of the {@link SchemeData} instance with the given scheme type. + * + * @param type A protection scheme type. + * @return A copy of the {@link SchemeData} instance with the given scheme type. + */ + public SchemeData copyWithSchemeType(String type) { + if (Util.areEqual(this.type, type)) { + return this; + } + return new SchemeData(uuid, type, mimeType, data, requiresSecureDecryption); + } + @Override public boolean equals(Object obj) { if (!(obj instanceof SchemeData)) { @@ -231,13 +281,14 @@ public final class DrmInitData implements Comparator, Parcelable { } SchemeData other = (SchemeData) obj; return mimeType.equals(other.mimeType) && Util.areEqual(uuid, other.uuid) - && Arrays.equals(data, other.data); + && Util.areEqual(type, other.type) && Arrays.equals(data, other.data); } @Override public int hashCode() { if (hashCode == 0) { int result = uuid.hashCode(); + result = 31 * result + (type == null ? 0 : type.hashCode()); result = 31 * result + mimeType.hashCode(); result = 31 * result + Arrays.hashCode(data); hashCode = result; @@ -256,6 +307,7 @@ public final class DrmInitData implements Comparator, Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeLong(uuid.getMostSignificantBits()); dest.writeLong(uuid.getLeastSignificantBits()); + dest.writeString(type); dest.writeString(mimeType); dest.writeByteArray(data); dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 591c56f525..4c8ca177e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -586,7 +586,7 @@ public final class MatroskaExtractor implements Extractor { if (currentTrack.cryptoData == null) { throw new ParserException("Encrypted Track found but ContentEncKeyID was not found"); } - currentTrack.drmInitData = new DrmInitData(new SchemeData(C.UUID_NIL, + currentTrack.drmInitData = new DrmInitData(new SchemeData(C.UUID_NIL, null, MimeTypes.VIDEO_WEBM, currentTrack.cryptoData.encryptionKey)); } break; 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 474ba65d86..65a3d87b45 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 @@ -615,10 +615,10 @@ import java.util.List; || childAtomType == Atom.TYPE_wvtt || childAtomType == Atom.TYPE_stpp || childAtomType == Atom.TYPE_c608) { parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, - language, drmInitData, out); + language, out); } else if (childAtomType == Atom.TYPE_camm) { out.format = Format.createSampleFormat(Integer.toString(trackId), - MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, drmInitData); + MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, null); } stsd.setPosition(childStartPosition + childAtomSize); } @@ -626,8 +626,7 @@ import java.util.List; } private static void parseTextSampleEntry(ParsableByteArray parent, int atomType, int position, - int atomSize, int trackId, String language, DrmInitData drmInitData, StsdData out) - throws ParserException { + int atomSize, int trackId, String language, StsdData out) throws ParserException { parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); // Default values. @@ -658,8 +657,7 @@ import java.util.List; } out.format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, - Format.NO_VALUE, 0, language, Format.NO_VALUE, drmInitData, subsampleOffsetUs, - initializationData); + Format.NO_VALUE, 0, language, Format.NO_VALUE, null, subsampleOffsetUs, initializationData); } private static void parseVideoSampleEntry(ParsableByteArray parent, int atomType, int position, @@ -677,7 +675,14 @@ import java.util.List; int childPosition = parent.getPosition(); if (atomType == Atom.TYPE_encv) { atomType = parseSampleEntryEncryptionData(parent, position, size, out, entryIndex); + TrackEncryptionBox encryptionBox = out.trackEncryptionBoxes[entryIndex]; + String schemeType = encryptionBox != null ? encryptionBox.schemeType : null; + if (schemeType != null) { + drmInitData = drmInitData.copyWithSchemeType(schemeType); + } parent.setPosition(childPosition); + } else { + drmInitData = null; } List initializationData = null; @@ -846,7 +851,14 @@ import java.util.List; int childPosition = parent.getPosition(); if (atomType == Atom.TYPE_enca) { atomType = parseSampleEntryEncryptionData(parent, position, size, out, entryIndex); + TrackEncryptionBox encryptionBox = out.trackEncryptionBoxes[entryIndex]; + String schemeType = encryptionBox != null ? encryptionBox.schemeType : null; + if (schemeType != null) { + drmInitData = drmInitData.copyWithSchemeType(schemeType); + } parent.setPosition(childPosition); + } else { + drmInitData = null; } // If the atom type determines a MIME type, set it immediately. @@ -1051,9 +1063,9 @@ import java.util.List; private static Pair parseSinfFromParent(ParsableByteArray parent, int position, int size) { int childPosition = position + Atom.HEADER_SIZE; - - boolean isCencScheme = false; - TrackEncryptionBox trackEncryptionBox = null; + int schemeInformationBoxPosition = C.POSITION_UNSET; + int schemeInformationBoxSize = 0; + String schemeType = null; Integer dataFormat = null; while (childPosition - position < size) { parent.setPosition(childPosition); @@ -1063,24 +1075,30 @@ import java.util.List; dataFormat = parent.readInt(); } else if (childAtomType == Atom.TYPE_schm) { parent.skipBytes(4); - isCencScheme = parent.readInt() == TYPE_cenc; + // scheme_type field. Defined in ISO/IEC 23001-7:2016, section 4.1. + schemeType = parent.readString(4); } else if (childAtomType == Atom.TYPE_schi) { - trackEncryptionBox = parseSchiFromParent(parent, childPosition, childAtomSize); + schemeInformationBoxPosition = childPosition; + schemeInformationBoxSize = childAtomSize; } childPosition += childAtomSize; } - if (isCencScheme) { + if (schemeType != null) { Assertions.checkArgument(dataFormat != null, "frma atom is mandatory"); - Assertions.checkArgument(trackEncryptionBox != null, "schi->tenc atom is mandatory"); - return Pair.create(dataFormat, trackEncryptionBox); + Assertions.checkArgument(schemeInformationBoxPosition != C.POSITION_UNSET, + "schi atom is mandatory"); + TrackEncryptionBox encryptionBox = parseSchiFromParent(parent, schemeInformationBoxPosition, + schemeInformationBoxSize, schemeType); + Assertions.checkArgument(encryptionBox != null, "tenc atom is mandatory"); + return Pair.create(dataFormat, encryptionBox); } else { return null; } } private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position, - int size) { + int size, String schemeType) { int childPosition = position + Atom.HEADER_SIZE; while (childPosition - position < size) { parent.setPosition(childPosition); @@ -1092,7 +1110,8 @@ import java.util.List; int defaultInitVectorSize = parent.readUnsignedByte(); byte[] defaultKeyId = new byte[16]; parent.readBytes(defaultKeyId, 0, defaultKeyId.length); - return new TrackEncryptionBox(defaultIsEncrypted, defaultInitVectorSize, defaultKeyId); + return new TrackEncryptionBox(defaultIsEncrypted, schemeType, defaultInitVectorSize, + defaultKeyId); } childPosition += childAtomSize; } 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 fe1d4b04af..5d44e71880 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 @@ -559,11 +559,12 @@ public final class FragmentedMp4Extractor implements Extractor { parseTruns(traf, trackBundle, decodeTime, flags); + TrackEncryptionBox encryptionBox = trackBundle.track + .getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); + LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); if (saiz != null) { - TrackEncryptionBox trackEncryptionBox = trackBundle.track - .sampleDescriptionEncryptionBoxes[fragment.header.sampleDescriptionIndex]; - parseSaiz(trackEncryptionBox, saiz.data, fragment); + parseSaiz(encryptionBox, saiz.data, fragment); } LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio); @@ -579,7 +580,8 @@ public final class FragmentedMp4Extractor implements Extractor { LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp); LeafAtom sgpd = traf.getLeafAtomOfType(Atom.TYPE_sgpd); if (sbgp != null && sgpd != null) { - parseSgpd(sbgp.data, sgpd.data, fragment); + parseSgpd(sbgp.data, sgpd.data, encryptionBox != null ? encryptionBox.schemeType : null, + fragment); } int leafChildrenSize = traf.leafChildren.size(); @@ -868,8 +870,8 @@ public final class FragmentedMp4Extractor implements Extractor { out.fillEncryptionData(senc); } - private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, TrackFragment out) - throws ParserException { + private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, String schemeType, + TrackFragment out) throws ParserException { sbgp.setPosition(Atom.HEADER_SIZE); int sbgpFullAtom = sbgp.readInt(); if (sbgp.readInt() != SAMPLE_GROUP_TYPE_seig) { @@ -910,7 +912,7 @@ public final class FragmentedMp4Extractor implements Extractor { byte[] keyId = new byte[16]; sgpd.readBytes(keyId, 0, keyId.length); out.definesEncryptionData = true; - out.trackEncryptionBox = new TrackEncryptionBox(isProtected, initVectorSize, keyId); + out.trackEncryptionBox = new TrackEncryptionBox(isProtected, schemeType, initVectorSize, keyId); } /** @@ -1135,7 +1137,7 @@ public final class FragmentedMp4Extractor implements Extractor { if (fragment.definesEncryptionData) { encryptionBox = fragment.trackEncryptionBox != null ? fragment.trackEncryptionBox - : track.sampleDescriptionEncryptionBoxes[fragment.header.sampleDescriptionIndex]; + : track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); if (encryptionBox != currentTrackBundle.cachedEncryptionBox) { cryptoData = new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, encryptionBox.keyId); } else { @@ -1205,7 +1207,7 @@ public final class FragmentedMp4Extractor implements Extractor { int sampleDescriptionIndex = trackFragment.header.sampleDescriptionIndex; TrackEncryptionBox encryptionBox = trackFragment.trackEncryptionBox != null ? trackFragment.trackEncryptionBox - : trackBundle.track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; + : trackBundle.track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex); int vectorSize = encryptionBox.initializationVectorSize; boolean subsampleEncryption = trackFragment .sampleHasSubsampleEncryptionTable[trackBundle.currentSampleIndex]; @@ -1245,7 +1247,7 @@ public final class FragmentedMp4Extractor implements Extractor { if (uuid == null) { Log.w(TAG, "Skipped pssh atom (failed to extract uuid)"); } else { - schemeDatas.add(new SchemeData(uuid, MimeTypes.VIDEO_MP4, psshData)); + schemeDatas.add(new SchemeData(uuid, null, MimeTypes.VIDEO_MP4, psshData)); } } } @@ -1325,8 +1327,12 @@ public final class FragmentedMp4Extractor implements Extractor { } public void updateDrmInitData(DrmInitData drmInitData) { - output.format(track.format.copyWithDrmInitData(drmInitData)); + TrackEncryptionBox encryptionBox = + track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); + String schemeType = encryptionBox != null ? encryptionBox.schemeType : null; + output.format(track.format.copyWithDrmInitData(drmInitData.copyWithSchemeType(schemeType))); } + } } 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 f1c4e99ec1..7ac3158794 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.mp4; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import java.lang.annotation.Retention; @@ -77,11 +78,6 @@ public final class Track { */ @Transformation public final int sampleTransformation; - /** - * Track encryption boxes for the different track sample descriptions. Entries may be null. - */ - public final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes; - /** * Durations of edit list segments in the movie timescale. Null if there is no edit list. */ @@ -98,9 +94,11 @@ public final class Track { */ public final int nalUnitLengthFieldLength; + @Nullable private final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes; + public Track(int id, int type, long timescale, long movieTimescale, long durationUs, Format format, @Transformation int sampleTransformation, - TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength, + @Nullable TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength, long[] editListDurations, long[] editListMediaTimes) { this.id = id; this.type = type; @@ -115,4 +113,16 @@ public final class Track { this.editListMediaTimes = editListMediaTimes; } + /** + * Returns the {@link TrackEncryptionBox} for the given sample description index. + * + * @param sampleDescriptionIndex The given sample description index + * @return The {@link TrackEncryptionBox} for the given sample description index. Maybe null if no + * such entry exists. + */ + 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 dde03a8507..d56504f780 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 @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.extractor.mp4; +import android.support.annotation.Nullable; + /** * Encapsulates information parsed from a track encryption (tenc) box or sample group description * (sgpd) box in an MP4 stream. @@ -26,6 +28,11 @@ public final class TrackEncryptionBox { */ public final boolean isEncrypted; + /** + * The protection scheme type, as defined by the 'schm' box, or null if unknown. + */ + @Nullable public final String schemeType; + /** * The initialization vector size in bytes for the samples in the corresponding sample group. */ @@ -37,13 +44,15 @@ public final class TrackEncryptionBox { public final byte[] keyId; /** - * @param isEncrypted Indicates the encryption state of the samples in the sample group. - * @param initializationVectorSize The initialization vector size in bytes for the samples in the - * corresponding sample group. - * @param keyId The key identifier for the samples in the corresponding sample group. + * @param isEncrypted See {@link #isEncrypted}. + * @param schemeType See {@link #schemeType}. + * @param initializationVectorSize See {@link #initializationVectorSize}. + * @param keyId See {@link #keyId}. */ - public TrackEncryptionBox(boolean isEncrypted, int initializationVectorSize, byte[] keyId) { + public TrackEncryptionBox(boolean isEncrypted, @Nullable String schemeType, + int initializationVectorSize, byte[] keyId) { this.isEncrypted = isEncrypted; + this.schemeType = schemeType; this.initializationVectorSize = initializationVectorSize; this.keyId = keyId; } diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java index d714573a82..bac1c272e8 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java @@ -75,7 +75,7 @@ public final class DashUtilTest extends TestCase { } private static DrmInitData newDrmInitData() { - return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType", + return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, null, "mimeType", new byte[]{1, 4, 7, 0, 3, 6})); } 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 0682af5dd6..53115a7a0e 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 @@ -343,6 +343,7 @@ public class DashManifestParser extends DefaultHandler IOException { String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); boolean isPlayReady = "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95".equals(schemeIdUri); + String schemeType = xpp.getAttributeValue(null, "value"); byte[] data = null; UUID uuid = null; boolean requiresSecureDecoder = false; @@ -368,8 +369,8 @@ public class DashManifestParser extends DefaultHandler requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); } } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); - return data != null ? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) - : null; + return data != null + ? new SchemeData(uuid, schemeType, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) : null; } /** 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 87f9c4d03b..8322da2471 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 @@ -69,7 +69,7 @@ import java.util.ArrayList; if (protectionElement != null) { byte[] keyId = getProtectionElementKeyId(protectionElement.data); trackEncryptionBoxes = new TrackEncryptionBox[] { - new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId)}; + new TrackEncryptionBox(true, null, INITIALIZATION_VECTOR_SIZE, keyId)}; } else { trackEncryptionBoxes = null; } 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 3ca5f8d997..5784cc7bc6 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 @@ -375,7 +375,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { StreamElement[] streamElementArray = new StreamElement[streamElements.size()]; streamElements.toArray(streamElementArray); if (protectionElement != null) { - DrmInitData drmInitData = new DrmInitData(new SchemeData(protectionElement.uuid, + DrmInitData drmInitData = new DrmInitData(new SchemeData(protectionElement.uuid, null, MimeTypes.VIDEO_MP4, protectionElement.data)); for (StreamElement streamElement : streamElementArray) { for (int i = 0; i < streamElement.formats.length; i++) { From ce8634507daf922c72b34e4cc0a35dbb931d3b8f Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 20 Jun 2017 02:54:20 -0700 Subject: [PATCH 140/220] Add a read reference into our linked list of Allocations ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159538997 --- .../exoplayer2/source/SampleQueue.java | 93 +++++++++++-------- 1 file changed, 53 insertions(+), 40 deletions(-) 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 1a2e65ceaa..ca266eb11d 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 @@ -65,6 +65,7 @@ public final class SampleQueue implements TrackOutput { // References into the linked list of allocations. private AllocationNode firstAllocationNode; + private AllocationNode readAllocationNode; private AllocationNode writeAllocationNode; // Accessed only by the consuming thread. @@ -89,6 +90,7 @@ public final class SampleQueue implements TrackOutput { scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); state = new AtomicInteger(); firstAllocationNode = new AllocationNode(0, allocationLength); + readAllocationNode = firstAllocationNode; writeAllocationNode = firstAllocationNode; } @@ -135,35 +137,33 @@ public final class SampleQueue implements TrackOutput { /** * Discards samples from the write side of the buffer. * - * @param discardFromIndex The absolute index of the first sample to be discarded. + * @param discardFromIndex The absolute index of the first sample to be discarded. Must be in the + * range [{@link #getReadIndex()}, {@link #getWriteIndex()}]. */ public void discardUpstreamSamples(int discardFromIndex) { totalBytesWritten = metadataQueue.discardUpstreamSamples(discardFromIndex); - dropUpstreamFrom(totalBytesWritten); - } - - /** - * Discards data from the write side of the buffer. Data is discarded from the specified absolute - * position. Any allocations that are fully discarded are returned to the allocator. - * - * @param absolutePosition The absolute position (inclusive) from which to discard data. - */ - private void dropUpstreamFrom(long absolutePosition) { - if (absolutePosition == firstAllocationNode.startPosition) { + if (totalBytesWritten == firstAllocationNode.startPosition) { clearAllocationNodes(firstAllocationNode); - firstAllocationNode = new AllocationNode(absolutePosition, allocationLength); + firstAllocationNode = new AllocationNode(totalBytesWritten, allocationLength); + readAllocationNode = firstAllocationNode; writeAllocationNode = firstAllocationNode; } else { - AllocationNode newWriteAllocationNode = firstAllocationNode; - AllocationNode currentNode = firstAllocationNode.next; - while (absolutePosition > currentNode.startPosition) { - newWriteAllocationNode = currentNode; - currentNode = currentNode.next; + // Find the last node containing at least 1 byte of data that we need to keep. + AllocationNode lastNodeToKeep = firstAllocationNode; + while (totalBytesWritten > lastNodeToKeep.endPosition) { + lastNodeToKeep = lastNodeToKeep.next; + } + // Discard all subsequent nodes. + AllocationNode firstNodeToDiscard = lastNodeToKeep.next; + clearAllocationNodes(firstNodeToDiscard); + // Reset the successor of the last node to be an uninitialized node. + lastNodeToKeep.next = new AllocationNode(lastNodeToKeep.endPosition, allocationLength); + // Update writeAllocationNode and readAllocationNode as necessary. + writeAllocationNode = totalBytesWritten == lastNodeToKeep.endPosition ? lastNodeToKeep.next + : lastNodeToKeep; + if (readAllocationNode == firstNodeToDiscard) { + readAllocationNode = lastNodeToKeep.next; } - clearAllocationNodes(currentNode); - writeAllocationNode = newWriteAllocationNode; - writeAllocationNode.next = new AllocationNode(writeAllocationNode.endPosition, - allocationLength); } } @@ -228,6 +228,7 @@ public final class SampleQueue implements TrackOutput { */ public void skipAll() { metadataQueue.skipAll(); + // TODO - Remove the following block and expose explicit discard operations. long nextOffset = metadataQueue.discardToRead(); if (nextOffset != C.POSITION_UNSET) { dropDownstreamTo(nextOffset); @@ -248,6 +249,7 @@ public final class SampleQueue implements TrackOutput { public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) { boolean success = metadataQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer); if (success) { + // TODO - Remove the following block and expose explicit discard operations. long nextOffset = metadataQueue.discardToRead(); if (nextOffset != C.POSITION_UNSET) { dropDownstreamTo(nextOffset); @@ -294,7 +296,7 @@ public final class SampleQueue implements TrackOutput { // Write the sample data into the holder. buffer.ensureSpaceForWrite(extrasHolder.size); readData(extrasHolder.offset, buffer.data, extrasHolder.size); - // Advance the read head. + // TODO - Remove the following block and expose explicit discard operations. long nextOffset = metadataQueue.discardToRead(); if (nextOffset != C.POSITION_UNSET) { dropDownstreamTo(nextOffset); @@ -390,17 +392,16 @@ public final class SampleQueue implements TrackOutput { * @param length The number of bytes to read. */ private void readData(long absolutePosition, ByteBuffer target, int length) { + advanceReadTo(absolutePosition); int remaining = length; - dropDownstreamTo(absolutePosition); while (remaining > 0) { - int toCopy = Math.min(remaining, (int) (firstAllocationNode.endPosition - absolutePosition)); - Allocation allocation = firstAllocationNode.allocation; - target.put(allocation.data, firstAllocationNode.translateOffset(absolutePosition), toCopy); + int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); + Allocation allocation = readAllocationNode.allocation; + target.put(allocation.data, readAllocationNode.translateOffset(absolutePosition), toCopy); remaining -= toCopy; absolutePosition += toCopy; - if (absolutePosition == firstAllocationNode.endPosition) { - allocator.release(allocation); - firstAllocationNode = firstAllocationNode.clear(); + if (absolutePosition == readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; } } } @@ -413,27 +414,38 @@ public final class SampleQueue implements TrackOutput { * @param length The number of bytes to read. */ private void readData(long absolutePosition, byte[] target, int length) { + advanceReadTo(absolutePosition); int remaining = length; - dropDownstreamTo(absolutePosition); while (remaining > 0) { - int toCopy = Math.min(remaining, (int) (firstAllocationNode.endPosition - absolutePosition)); - Allocation allocation = firstAllocationNode.allocation; - System.arraycopy(allocation.data, firstAllocationNode.translateOffset(absolutePosition), + int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); + Allocation allocation = readAllocationNode.allocation; + System.arraycopy(allocation.data, readAllocationNode.translateOffset(absolutePosition), target, length - remaining, toCopy); remaining -= toCopy; absolutePosition += toCopy; - if (absolutePosition == firstAllocationNode.endPosition) { - allocator.release(allocation); - firstAllocationNode = firstAllocationNode.clear(); + if (absolutePosition == readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; } } } /** - * Discard any allocations that hold data prior to the specified absolute position, returning - * them to the allocator. + * Advances {@link #readAllocationNode} to the specified absolute position. * - * @param absolutePosition The absolute position up to which allocations can be discarded. + * @param absolutePosition The position to which {@link #readAllocationNode} should be advanced. + */ + private void advanceReadTo(long absolutePosition) { + while (absolutePosition >= readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; + } + } + + /** + * Advances {@link #firstAllocationNode} to the specified absolute position. Nodes that are + * advanced over are cleared, and their underlying allocations are returned to the allocator. + * + * @param absolutePosition The position to which {@link #firstAllocationNode} should be advanced. + * Must never exceed the absolute position of the next sample to be read. */ private void dropDownstreamTo(long absolutePosition) { while (absolutePosition >= firstAllocationNode.endPosition) { @@ -564,6 +576,7 @@ public final class SampleQueue implements TrackOutput { metadataQueue.clearSampleData(); clearAllocationNodes(firstAllocationNode); firstAllocationNode = new AllocationNode(0, allocationLength); + readAllocationNode = firstAllocationNode; writeAllocationNode = firstAllocationNode; totalBytesWritten = 0; allocator.trim(); From 1c8f14b5fd68872da92eac7343f9735fc2859b1f Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 20 Jun 2017 04:43:03 -0700 Subject: [PATCH 141/220] Add support to run actions into FakeDataSource An actions is triggered when the reading reaches action's position. This can be used to make sure the code is in a certain state while testing. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159545923 --- .../exoplayer2/testutil/FakeDataSource.java | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java index 3e4d6b0440..db11cc98a3 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java @@ -49,6 +49,10 @@ import java.util.HashMap; * until the source is closed. If the source is closed and re-opened having encountered an error, * that error will not be thrown again. * + *

        Actions are inserted by calling {@link FakeData#appendReadAction(Runnable)}. An actions is + * triggered when the reading reaches action's position. This can be used to make sure the code is + * in a certain state while testing. + * *

        Example usage: * *

        @@ -165,6 +169,9 @@ public final class FakeDataSource implements DataSource {
                 } else {
                   currentSegmentIndex++;
                 }
        +      } else if (current.isActionSegment()) {
        +        currentSegmentIndex++;
        +        current.action.run();
               } else {
                 // Read at most bytesRemaining.
                 readLength = (int) Math.min(readLength, bytesRemaining);
        @@ -217,21 +224,41 @@ public final class FakeDataSource implements DataSource {
             public final IOException exception;
             public final byte[] data;
             public final int length;
        +    public final Runnable action;
         
             private boolean exceptionThrown;
             private boolean exceptionCleared;
             private int bytesRead;
         
        -    public Segment(byte[] data, IOException exception) {
        +    public Segment(byte[] data) {
               this.data = data;
        +      this.length = data.length;
        +      this.exception = null;
        +      this.action = null;
        +    }
        +
        +    public Segment(IOException exception) {
        +      this.data = null;
        +      this.length = 0;
               this.exception = exception;
        -      length = data != null ? data.length : 0;
        +      this.action = null;
        +    }
        +
        +    public Segment(Runnable action) {
        +      this.data = null;
        +      this.length = 0;
        +      this.exception = null;
        +      this.action = action;
             }
         
             public boolean isErrorSegment() {
               return exception != null;
             }
         
        +    public boolean isActionSegment() {
        +      return action != null;
        +    }
        +
           }
         
           /** Container of fake data to be served by a {@link FakeDataSource}. */
        @@ -270,7 +297,7 @@ public final class FakeDataSource implements DataSource {
              */
             public FakeData appendReadData(byte[] data) {
               Assertions.checkState(data != null && data.length > 0);
        -      segments.add(new Segment(data, null));
        +      segments.add(new Segment(data));
               return this;
             }
         
        @@ -278,7 +305,15 @@ public final class FakeDataSource implements DataSource {
              * Appends an error in the underlying data.
              */
             public FakeData appendReadError(IOException exception) {
        -      segments.add(new Segment(null, exception));
        +      segments.add(new Segment(exception));
        +      return this;
        +    }
        +
        +    /**
        +     * Appends an action.
        +     */
        +    public FakeData appendReadAction(Runnable action) {
        +      segments.add(new Segment(action));
               return this;
             }
         
        
        From 8d74ba4850df39c8ef8cb26e4d3b5b8bbf96be38 Mon Sep 17 00:00:00 2001
        From: aquilescanta 
        Date: Tue, 20 Jun 2017 06:32:12 -0700
        Subject: [PATCH 142/220] Add support for cbc1 encryption scheme
        
        Issue:#1661
        Issue:#1989
        Issue:#2089
        
        -------------
        Created by MOE: https://github.com/google/moe
        MOE_MIGRATED_REVID=159553419
        ---
         demo/src/main/assets/media.exolist.json       | 32 ++++++++++++++--
         .../extractor/mp4/FragmentedMp4Extractor.java | 22 +++--------
         .../extractor/mp4/TrackEncryptionBox.java     | 37 ++++++++++++++++---
         3 files changed, 64 insertions(+), 27 deletions(-)
        
        diff --git a/demo/src/main/assets/media.exolist.json b/demo/src/main/assets/media.exolist.json
        index 4a51919657..59ae87ad5f 100644
        --- a/demo/src/main/assets/media.exolist.json
        +++ b/demo/src/main/assets/media.exolist.json
        @@ -138,28 +138,52 @@
                 "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd"
               },
               {
        -        "name": "WV: Secure SD & HD (MP4,H264)",
        +        "name": "WV: Secure SD & HD (cenc,MP4,H264)",
                 "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
                 "drm_scheme": "widevine",
                 "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
               },
               {
        -        "name": "WV: Secure SD (MP4,H264)",
        +        "name": "WV: Secure SD (cenc,MP4,H264)",
                 "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
                 "drm_scheme": "widevine",
                 "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
               },
               {
        -        "name": "WV: Secure HD (MP4,H264)",
        +        "name": "WV: Secure HD (cenc,MP4,H264)",
                 "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd",
                 "drm_scheme": "widevine",
                 "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
               },
               {
        -        "name": "WV: Secure UHD (MP4,H264)",
        +        "name": "WV: Secure UHD (cenc,MP4,H264)",
                 "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
                 "drm_scheme": "widevine",
                 "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
        +      },
        +      {
        +        "name": "WV: Secure SD & HD (cbc1,MP4,H264)",
        +        "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd",
        +        "drm_scheme": "widevine",
        +        "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
        +      },
        +      {
        +        "name": "WV: Secure SD (cbc1,MP4,H264)",
        +        "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_sd.mpd",
        +        "drm_scheme": "widevine",
        +        "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
        +      },
        +      {
        +        "name": "WV: Secure HD (cbc1,MP4,H264)",
        +        "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_hd.mpd",
        +        "drm_scheme": "widevine",
        +        "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
        +      },
        +      {
        +        "name": "WV: Secure UHD (cbc1,MP4,H264)",
        +        "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd",
        +        "drm_scheme": "widevine",
        +        "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
               }
             ]
           },
        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 5d44e71880..cc3f315014 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
        @@ -1128,24 +1128,18 @@ public final class FragmentedMp4Extractor implements Extractor {
               sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs);
             }
         
        -    @C.BufferFlags int sampleFlags = (fragment.definesEncryptionData ? C.BUFFER_FLAG_ENCRYPTED : 0)
        -        | (fragment.sampleIsSyncFrameTable[sampleIndex] ? C.BUFFER_FLAG_KEY_FRAME : 0);
        +    @C.BufferFlags int sampleFlags = fragment.sampleIsSyncFrameTable[sampleIndex]
        +        ? C.BUFFER_FLAG_KEY_FRAME : 0;
         
             // Encryption data.
             TrackOutput.CryptoData cryptoData = null;
        -    TrackEncryptionBox encryptionBox = null;
             if (fragment.definesEncryptionData) {
        -      encryptionBox = fragment.trackEncryptionBox != null
        +      sampleFlags |= C.BUFFER_FLAG_ENCRYPTED;
        +      TrackEncryptionBox encryptionBox = fragment.trackEncryptionBox != null
                   ? fragment.trackEncryptionBox
                   : track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex);
        -      if (encryptionBox != currentTrackBundle.cachedEncryptionBox) {
        -        cryptoData = new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, encryptionBox.keyId);
        -      } else {
        -        cryptoData = currentTrackBundle.cachedCryptoData;
        -      }
        +      cryptoData = encryptionBox.cryptoData;
             }
        -    currentTrackBundle.cachedCryptoData = cryptoData;
        -    currentTrackBundle.cachedEncryptionBox = encryptionBox;
         
             output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, cryptoData);
         
        @@ -1301,10 +1295,6 @@ public final class FragmentedMp4Extractor implements Extractor {
             public int currentSampleInTrackRun;
             public int currentTrackRunIndex;
         
        -    // Auxiliary references.
        -    public TrackOutput.CryptoData cachedCryptoData;
        -    public TrackEncryptionBox cachedEncryptionBox;
        -
             public TrackBundle(TrackOutput output) {
               fragment = new TrackFragment();
               this.output = output;
        @@ -1322,8 +1312,6 @@ public final class FragmentedMp4Extractor implements Extractor {
               currentSampleIndex = 0;
               currentTrackRunIndex = 0;
               currentSampleInTrackRun = 0;
        -      cachedCryptoData = null;
        -      cachedEncryptionBox = null;
             }
         
             public void updateDrmInitData(DrmInitData drmInitData) {
        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 d56504f780..6f33d2222f 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
        @@ -16,6 +16,9 @@
         package com.google.android.exoplayer2.extractor.mp4;
         
         import android.support.annotation.Nullable;
        +import android.util.Log;
        +import com.google.android.exoplayer2.C;
        +import com.google.android.exoplayer2.extractor.TrackOutput;
         
         /**
          * Encapsulates information parsed from a track encryption (tenc) box or sample group description 
        @@ -23,6 +26,8 @@ import android.support.annotation.Nullable;
          */
         public final class TrackEncryptionBox {
         
        +  private static final String TAG = "TrackEncryptionBox";
        +
           /**
            * Indicates the encryption state of the samples in the sample group.
            */
        @@ -33,28 +38,48 @@ public final class TrackEncryptionBox {
            */
           @Nullable public final String schemeType;
         
        +  /**
        +   * A {@link TrackOutput.CryptoData} instance containing the encryption information from this
        +   * {@link TrackEncryptionBox}.
        +   */
        +  public final TrackOutput.CryptoData cryptoData;
        +
           /**
            * The initialization vector size in bytes for the samples in the corresponding sample group.
            */
           public final int initializationVectorSize;
         
        -  /**
        -   * The key identifier for the samples in the corresponding sample group.
        -   */
        -  public final byte[] keyId;
         
           /**
            * @param isEncrypted See {@link #isEncrypted}.
            * @param schemeType See {@link #schemeType}.
            * @param initializationVectorSize See {@link #initializationVectorSize}.
        -   * @param keyId See {@link #keyId}.
        +   * @param keyId See {@link TrackOutput.CryptoData#encryptionKey}.
            */
           public TrackEncryptionBox(boolean isEncrypted, @Nullable String schemeType,
               int initializationVectorSize, byte[] keyId) {
             this.isEncrypted = isEncrypted;
             this.schemeType = schemeType;
             this.initializationVectorSize = initializationVectorSize;
        -    this.keyId = keyId;
        +    cryptoData = new TrackOutput.CryptoData(schemeToCryptoMode(schemeType), keyId);
        +  }
        +
        +  @C.CryptoMode
        +  private static int schemeToCryptoMode(@Nullable String schemeType) {
        +    if (schemeType == null) {
        +      // If unknown, assume cenc.
        +      return C.CRYPTO_MODE_AES_CTR;
        +    }
        +    switch (schemeType) {
        +      case "cenc":
        +        return C.CRYPTO_MODE_AES_CTR;
        +      case "cbc1":
        +        return C.CRYPTO_MODE_AES_CBC;
        +      default:
        +        Log.w(TAG, "Unsupported protection scheme type '" + schemeType + "'. Assuming AES-CTR "
        +            + "crypto mode.");
        +        return C.CRYPTO_MODE_AES_CTR;
        +    }
           }
         
         }
        
        From 0ffc3ffd291969194f30903630f748caf794d7ba Mon Sep 17 00:00:00 2001
        From: aquilescanta 
        Date: Tue, 20 Jun 2017 06:48:44 -0700
        Subject: [PATCH 143/220] Fix DrmInitDataTest
        
        -------------
        Created by MOE: https://github.com/google/moe
        MOE_MIGRATED_REVID=159554717
        ---
         .../java/com/google/android/exoplayer2/drm/DrmInitDataTest.java | 2 +-
         1 file changed, 1 insertion(+), 1 deletion(-)
        
        diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java
        index b7f1cd1ed6..aa8cbfdb62 100644
        --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java
        +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java
        @@ -35,7 +35,7 @@ public class DrmInitDataTest extends TestCase {
               TestUtil.buildTestData(128, 1 /* data seed */));
           private static final SchemeData DATA_2 = new SchemeData(PLAYREADY_UUID,  null, VIDEO_MP4,
               TestUtil.buildTestData(128, 2 /* data seed */));
        -  private static final SchemeData DATA_1B = new SchemeData(WIDEVINE_UUID, "cens", VIDEO_MP4,
        +  private static final SchemeData DATA_1B = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4,
               TestUtil.buildTestData(128, 1 /* data seed */));
           private static final SchemeData DATA_2B = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4,
               TestUtil.buildTestData(128, 2 /* data seed */));
        
        From 467fd2535c030171a0520e4884de76ecd9308731 Mon Sep 17 00:00:00 2001
        From: olly 
        Date: Tue, 20 Jun 2017 07:01:35 -0700
        Subject: [PATCH 144/220] Expose new non-discarding SampleQueue read/skip
         methods
        
        -------------
        Created by MOE: https://github.com/google/moe
        MOE_MIGRATED_REVID=159555748
        ---
         .../exoplayer2/source/SampleQueueTest.java    | 70 ++++++++++++++---
         .../exoplayer2/source/SampleQueue.java        | 75 +++++++++++++------
         2 files changed, 112 insertions(+), 33 deletions(-)
        
        diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java
        index 129f299779..8723e39020 100644
        --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java
        +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java
        @@ -133,6 +133,9 @@ public class SampleQueueTest extends TestCase {
             assertReadFormat(true, TEST_FORMAT_1);
             // Otherwise should read the sample.
             assertSampleRead(1000, true, TEST_DATA, 0, ALLOCATION_SIZE);
        +    // Allocation should still be held.
        +    assertAllocationCount(1);
        +    sampleQueue.discardToRead();
             // The allocation should have been released.
             assertAllocationCount(0);
         
        @@ -147,6 +150,9 @@ public class SampleQueueTest extends TestCase {
             assertReadFormat(true, TEST_FORMAT_1);
             // Read the sample.
             assertSampleRead(2000, false, TEST_DATA, 0, ALLOCATION_SIZE - 1);
        +    // Allocation should still be held.
        +    assertAllocationCount(1);
        +    sampleQueue.discardToRead();
             // The last byte written to the sample queue may belong to a sample whose metadata has yet to be
             // written, so an allocation should still be held.
             assertAllocationCount(1);
        @@ -158,6 +164,9 @@ public class SampleQueueTest extends TestCase {
             assertReadFormat(true, TEST_FORMAT_1);
             // Read the sample.
             assertSampleRead(3000, false, TEST_DATA, ALLOCATION_SIZE - 1, 1);
        +    // Allocation should still be held.
        +    assertAllocationCount(1);
        +    sampleQueue.discardToRead();
             // The allocation should have been released.
             assertAllocationCount(0);
           }
        @@ -168,6 +177,8 @@ public class SampleQueueTest extends TestCase {
                 sampleQueue.getLargestQueuedTimestampUs());
             assertAllocationCount(10);
             assertReadTestData();
        +    assertAllocationCount(10);
        +    sampleQueue.discardToRead();
             assertAllocationCount(0);
           }
         
        @@ -177,12 +188,42 @@ public class SampleQueueTest extends TestCase {
             assertAllocationCount(20);
             assertReadTestData(TEST_FORMAT_2);
             assertReadTestData(TEST_FORMAT_2);
        +    assertAllocationCount(20);
        +    sampleQueue.discardToRead();
             assertAllocationCount(0);
           }
         
        +  public void testReadMultiWithRewind() {
        +    writeTestData();
        +    assertReadTestData();
        +    assertEquals(8, sampleQueue.getReadIndex());
        +    assertAllocationCount(10);
        +    // Rewind.
        +    sampleQueue.rewind();
        +    assertAllocationCount(10);
        +    // Read again.
        +    assertEquals(0, sampleQueue.getReadIndex());
        +    assertReadTestData();
        +  }
        +
        +  public void testRewindAfterDiscard() {
        +    writeTestData();
        +    assertReadTestData();
        +    sampleQueue.discardToRead();
        +    assertAllocationCount(0);
        +    // Rewind.
        +    sampleQueue.rewind();
        +    assertAllocationCount(0);
        +    // Can't read again.
        +    assertEquals(8, sampleQueue.getReadIndex());
        +    assertReadEndOfStream(false);
        +  }
        +
           public void testSkipAll() {
             writeTestData();
        -    sampleQueue.skipAll();
        +    sampleQueue.skipAll2();
        +    assertAllocationCount(10);
        +    sampleQueue.discardToRead();
             assertAllocationCount(0);
             // Despite skipping all samples, we should still read the last format, since this is the
             // expected format for a subsequent sample.
        @@ -194,7 +235,9 @@ public class SampleQueueTest extends TestCase {
           public void testSkipAllRetainsUnassignedData() {
             sampleQueue.format(TEST_FORMAT_1);
             sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE);
        -    sampleQueue.skipAll();
        +    sampleQueue.skipAll2();
        +    assertAllocationCount(1);
        +    sampleQueue.discardToRead();
             // Skipping shouldn't discard data that may belong to a sample whose metadata has yet to be
             // written.
             assertAllocationCount(1);
        @@ -207,12 +250,14 @@ public class SampleQueueTest extends TestCase {
             // Once the metadata has been written, check the sample can be read as expected.
             assertSampleRead(0, true, TEST_DATA, 0, ALLOCATION_SIZE);
             assertNoSamplesToRead(TEST_FORMAT_1);
        +    assertAllocationCount(1);
        +    sampleQueue.discardToRead();
             assertAllocationCount(0);
           }
         
           public void testSkipToKeyframeBeforeBuffer() {
             writeTestData();
        -    boolean result = sampleQueue.skipToKeyframeBefore(TEST_SAMPLE_TIMESTAMPS[0] - 1, false);
        +    boolean result = sampleQueue.skipToKeyframeBefore2(TEST_SAMPLE_TIMESTAMPS[0] - 1, false);
             // Should fail and have no effect.
             assertFalse(result);
             assertReadTestData();
        @@ -221,7 +266,7 @@ public class SampleQueueTest extends TestCase {
         
           public void testSkipToKeyframeStartOfBuffer() {
             writeTestData();
        -    boolean result = sampleQueue.skipToKeyframeBefore(TEST_SAMPLE_TIMESTAMPS[0], false);
        +    boolean result = sampleQueue.skipToKeyframeBefore2(TEST_SAMPLE_TIMESTAMPS[0], false);
             // Should succeed but have no effect (we're already at the first frame).
             assertTrue(result);
             assertReadTestData();
        @@ -230,7 +275,7 @@ public class SampleQueueTest extends TestCase {
         
           public void testSkipToKeyframeEndOfBuffer() {
             writeTestData();
        -    boolean result = sampleQueue.skipToKeyframeBefore(
        +    boolean result = sampleQueue.skipToKeyframeBefore2(
                 TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1], false);
             // Should succeed and skip to 2nd keyframe.
             assertTrue(result);
        @@ -240,7 +285,7 @@ public class SampleQueueTest extends TestCase {
         
           public void testSkipToKeyframeAfterBuffer() {
             writeTestData();
        -    boolean result = sampleQueue.skipToKeyframeBefore(
        +    boolean result = sampleQueue.skipToKeyframeBefore2(
                 TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1] + 1, false);
             // Should fail and have no effect.
             assertFalse(result);
        @@ -250,7 +295,7 @@ public class SampleQueueTest extends TestCase {
         
           public void testSkipToKeyframeAfterBufferAllowed() {
             writeTestData();
        -    boolean result = sampleQueue.skipToKeyframeBefore(
        +    boolean result = sampleQueue.skipToKeyframeBefore2(
                 TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1] + 1, true);
             // Should succeed and skip to 2nd keyframe.
             assertTrue(result);
        @@ -305,6 +350,8 @@ public class SampleQueueTest extends TestCase {
             writeTestData();
             assertReadTestData(null, 0, 3);
             sampleQueue.discardUpstreamSamples(8);
        +    assertAllocationCount(10);
        +    sampleQueue.discardToRead();
             assertAllocationCount(7);
             sampleQueue.discardUpstreamSamples(7);
             assertAllocationCount(6);
        @@ -359,6 +406,7 @@ public class SampleQueueTest extends TestCase {
            * Asserts correct reading of standard test data from {@code sampleQueue}.
            *
            * @param startFormat The format of the last sample previously read from {@code sampleQueue}.
        +   * @param firstSampleIndex The index of the first sample that's expected to be read.
            */
           private void assertReadTestData(Format startFormat, int firstSampleIndex) {
             assertReadTestData(startFormat, firstSampleIndex,
        @@ -428,7 +476,7 @@ public class SampleQueueTest extends TestCase {
            */
           private void assertReadNothing(boolean formatRequired) {
             clearFormatHolderAndInputBuffer();
        -    int result = sampleQueue.readData(formatHolder, inputBuffer, formatRequired, false, 0);
        +    int result = sampleQueue.readData2(formatHolder, inputBuffer, formatRequired, false, 0);
             assertEquals(C.RESULT_NOTHING_READ, result);
             // formatHolder should not be populated.
             assertNull(formatHolder.format);
        @@ -445,7 +493,7 @@ public class SampleQueueTest extends TestCase {
            */
           private void assertReadEndOfStream(boolean formatRequired) {
             clearFormatHolderAndInputBuffer();
        -    int result = sampleQueue.readData(formatHolder, inputBuffer, formatRequired, true, 0);
        +    int result = sampleQueue.readData2(formatHolder, inputBuffer, formatRequired, true, 0);
             assertEquals(C.RESULT_BUFFER_READ, result);
             // formatHolder should not be populated.
             assertNull(formatHolder.format);
        @@ -465,7 +513,7 @@ public class SampleQueueTest extends TestCase {
            */
           private void assertReadFormat(boolean formatRequired, Format format) {
             clearFormatHolderAndInputBuffer();
        -    int result = sampleQueue.readData(formatHolder, inputBuffer, formatRequired, false, 0);
        +    int result = sampleQueue.readData2(formatHolder, inputBuffer, formatRequired, false, 0);
             assertEquals(C.RESULT_FORMAT_READ, result);
             // formatHolder should be populated.
             assertEquals(format, formatHolder.format);
        @@ -487,7 +535,7 @@ public class SampleQueueTest extends TestCase {
           private void assertSampleRead(long timeUs, boolean isKeyframe, byte[] sampleData, int offset,
               int length) {
             clearFormatHolderAndInputBuffer();
        -    int result = sampleQueue.readData(formatHolder, inputBuffer, false, false, 0);
        +    int result = sampleQueue.readData2(formatHolder, inputBuffer, false, false, 0);
             assertEquals(C.RESULT_BUFFER_READ, result);
             // formatHolder should not be populated.
             assertNull(formatHolder.format);
        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 ca266eb11d..35a53c5cdf 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
        @@ -224,17 +224,50 @@ public final class SampleQueue implements TrackOutput {
           }
         
           /**
        -   * Skips all samples currently in the buffer.
        +   * Rewinds the read position to the first sample in the queue.
            */
        -  public void skipAll() {
        -    metadataQueue.skipAll();
        -    // TODO - Remove the following block and expose explicit discard operations.
        +  public void rewind() {
        +    metadataQueue.rewind();
        +    readAllocationNode = firstAllocationNode;
        +  }
        +
        +  /**
        +   * Discards samples up to the current read position.
        +   */
        +  public void discardToRead() {
             long nextOffset = metadataQueue.discardToRead();
             if (nextOffset != C.POSITION_UNSET) {
               dropDownstreamTo(nextOffset);
             }
           }
         
        +  /**
        +   * @deprecated Use {@link #skipAll2()} followed by {@link #discardToRead()}.
        +   */
        +  @Deprecated
        +  public void skipAll() {
        +    skipAll2();
        +    discardToRead();
        +  }
        +
        +  /**
        +   * Skips all samples currently in the buffer.
        +   */
        +  public void skipAll2() {
        +    metadataQueue.skipAll();
        +  }
        +
        +  /**
        +   * @deprecated Use {@link #skipToKeyframeBefore2(long, boolean)} followed by
        +   *     {@link #discardToRead()}.
        +   */
        +  @Deprecated
        +  public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
        +    boolean success = skipToKeyframeBefore2(timeUs, allowTimeBeyondBuffer);
        +    discardToRead();
        +    return success;
        +  }
        +
           /**
            * Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer
            * contains a keyframe with a timestamp of {@code timeUs} or earlier. If
        @@ -246,18 +279,21 @@ public final class SampleQueue implements TrackOutput {
            *     of the buffer.
            * @return Whether the skip was successful.
            */
        -  public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
        -    boolean success = metadataQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer);
        -    if (success) {
        -      // TODO - Remove the following block and expose explicit discard operations.
        -      long nextOffset = metadataQueue.discardToRead();
        -      if (nextOffset != C.POSITION_UNSET) {
        -        dropDownstreamTo(nextOffset);
        -      }
        -      return true;
        -    } else {
        -      return false;
        -    }
        +  public boolean skipToKeyframeBefore2(long timeUs, boolean allowTimeBeyondBuffer) {
        +    return metadataQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer);
        +  }
        +
        +  /**
        +   * @deprecated Use {@link #readData2(FormatHolder, DecoderInputBuffer, boolean, boolean, long)}
        +   *     followed by {@link #discardToRead()}.
        +   */
        +  @Deprecated
        +  public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired,
        +      boolean loadingFinished, long decodeOnlyUntilUs) {
        +    int result = readData2(formatHolder, buffer, formatRequired, loadingFinished,
        +        decodeOnlyUntilUs);
        +    discardToRead();
        +    return result;
           }
         
           /**
        @@ -276,7 +312,7 @@ public final class SampleQueue implements TrackOutput {
            * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
            *     {@link C#RESULT_BUFFER_READ}.
            */
        -  public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired,
        +  public int readData2(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired,
               boolean loadingFinished, long decodeOnlyUntilUs) {
             int result = metadataQueue.readData(formatHolder, buffer, formatRequired, loadingFinished,
                 downstreamFormat, extrasHolder);
        @@ -296,11 +332,6 @@ public final class SampleQueue implements TrackOutput {
                   // Write the sample data into the holder.
                   buffer.ensureSpaceForWrite(extrasHolder.size);
                   readData(extrasHolder.offset, buffer.data, extrasHolder.size);
        -          // TODO - Remove the following block and expose explicit discard operations.
        -          long nextOffset = metadataQueue.discardToRead();
        -          if (nextOffset != C.POSITION_UNSET) {
        -            dropDownstreamTo(nextOffset);
        -          }
                 }
                 return C.RESULT_BUFFER_READ;
               case C.RESULT_NOTHING_READ:
        
        From 44f6dbb0ccb856391e67ca96c5a8bdc5bbf7e067 Mon Sep 17 00:00:00 2001
        From: eguven 
        Date: Tue, 20 Jun 2017 09:02:46 -0700
        Subject: [PATCH 145/220] Work around the error prone warning
        
        Error prone check doesn't like we pass a variable named 'end' as start parameter and 'start' as end.
        
        -------------
        Created by MOE: https://github.com/google/moe
        MOE_MIGRATED_REVID=159567308
        ---
         .../java/com/google/android/exoplayer2/util/Util.java  | 10 +++++-----
         1 file changed, 5 insertions(+), 5 deletions(-)
        
        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 50932cdf48..7c01c59914 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
        @@ -977,15 +977,15 @@ public final class Util {
             int expectedLength = length - percentCharacterCount * 2;
             StringBuilder builder = new StringBuilder(expectedLength);
             Matcher matcher = ESCAPED_CHARACTER_PATTERN.matcher(fileName);
        -    int endOfLastMatch = 0;
        +    int startOfNotEscaped = 0;
             while (percentCharacterCount > 0 && matcher.find()) {
               char unescapedCharacter = (char) Integer.parseInt(matcher.group(1), 16);
        -      builder.append(fileName, endOfLastMatch, matcher.start()).append(unescapedCharacter);
        -      endOfLastMatch = matcher.end();
        +      builder.append(fileName, startOfNotEscaped, matcher.start()).append(unescapedCharacter);
        +      startOfNotEscaped = matcher.end();
               percentCharacterCount--;
             }
        -    if (endOfLastMatch < length) {
        -      builder.append(fileName, endOfLastMatch, length);
        +    if (startOfNotEscaped < length) {
        +      builder.append(fileName, startOfNotEscaped, length);
             }
             if (builder.length() != expectedLength) {
               return null;
        
        From 40039ed0a1a58e368916ed332933292597a97bac Mon Sep 17 00:00:00 2001
        From: eguven 
        Date: Wed, 21 Jun 2017 02:17:27 -0700
        Subject: [PATCH 146/220] Fix and move Util.getRemainderDataSpec() to
         DataSpec.subrange()
        
        Made the method copy all of the fields of DataSpec in to the new instance. Also converted
        it to an instance method of DataSpec for ease of usage, discovery and maintenance.
        
        -------------
        Created by MOE: https://github.com/google/moe
        MOE_MIGRATED_REVID=159670314
        ---
         .../source/chunk/ContainerMediaChunk.java     |  2 +-
         .../source/chunk/InitializationChunk.java     |  2 +-
         .../source/chunk/SingleSampleMediaChunk.java  |  2 +-
         .../android/exoplayer2/upstream/DataSpec.java | 29 ++++++++++++++++++-
         .../google/android/exoplayer2/util/Util.java  | 20 -------------
         .../exoplayer2/source/hls/HlsMediaChunk.java  |  6 ++--
         6 files changed, 34 insertions(+), 27 deletions(-)
        
        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 cfbefc0c2e..cc39c88fd0 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
        @@ -93,7 +93,7 @@ public class ContainerMediaChunk extends BaseMediaChunk {
           @SuppressWarnings("NonAtomicVolatileUpdate")
           @Override
           public final void load() throws IOException, InterruptedException {
        -    DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
        +    DataSpec loadDataSpec = dataSpec.subrange(bytesLoaded);
             try {
               // Create and open the input.
               ExtractorInput input = new DefaultExtractorInput(dataSource,
        diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
        index 69474aa150..4acf0b8525 100644
        --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
        +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
        @@ -72,7 +72,7 @@ public final class InitializationChunk extends Chunk {
           @SuppressWarnings("NonAtomicVolatileUpdate")
           @Override
           public void load() throws IOException, InterruptedException {
        -    DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
        +    DataSpec loadDataSpec = dataSpec.subrange(bytesLoaded);
             try {
               // Create and open the input.
               ExtractorInput input = new DefaultExtractorInput(dataSource,
        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 a008c9cd84..02cf7dfd55 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
        @@ -85,7 +85,7 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
           @SuppressWarnings("NonAtomicVolatileUpdate")
           @Override
           public void load() throws IOException, InterruptedException {
        -    DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
        +    DataSpec loadDataSpec = dataSpec.subrange(bytesLoaded);
             try {
               // Create and open the input.
               long length = dataSource.open(loadDataSpec);
        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 d3c63b4454..ab1542c7a6 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
        @@ -68,7 +68,7 @@ public final class DataSpec {
            * The position of the data when read from {@link #uri}.
            * 

        * Always equal to {@link #absoluteStreamPosition} unless the {@link #uri} defines the location - * of a subset of the underyling data. + * of a subset of the underlying data. */ public final long position; /** @@ -187,4 +187,31 @@ public final class DataSpec { + ", " + position + ", " + length + ", " + key + ", " + flags + "]"; } + /** + * Returns a {@link DataSpec} that represents a subrange of the data defined by this DataSpec. The + * subrange includes data from the offset up to the end of this DataSpec. + * + * @param offset The offset of the subrange. + * @return A {@link DataSpec} that represents a subrange of the data defined by this DataSpec. + */ + public DataSpec subrange(long offset) { + return subrange(offset, length == C.LENGTH_UNSET ? C.LENGTH_UNSET : length - offset); + } + + /** + * Returns a {@link DataSpec} that represents a subrange of the data defined by this DataSpec. + * + * @param offset The offset of the subrange. + * @param length The length of the subrange. + * @return A {@link DataSpec} that represents a subrange of the data defined by this DataSpec. + */ + public DataSpec subrange(long offset, long length) { + if (offset == 0 && this.length == length) { + return this; + } else { + return new DataSpec(uri, postBody, absoluteStreamPosition + offset, position + offset, length, + key, flags); + } + } + } 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 7c01c59914..a65a9cbf44 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 @@ -34,7 +34,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DataSpec; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; @@ -680,25 +679,6 @@ public final class Util { return intArray; } - /** - * Given a {@link DataSpec} and a number of bytes already loaded, returns a {@link DataSpec} - * that represents the remainder of the data. - * - * @param dataSpec The original {@link DataSpec}. - * @param bytesLoaded The number of bytes already loaded. - * @return A {@link DataSpec} that represents the remainder of the data. - */ - public static DataSpec getRemainderDataSpec(DataSpec dataSpec, int bytesLoaded) { - if (bytesLoaded == 0) { - return dataSpec; - } else { - long remainingLength = dataSpec.length == C.LENGTH_UNSET ? C.LENGTH_UNSET - : dataSpec.length - bytesLoaded; - return new DataSpec(dataSpec.uri, dataSpec.position + bytesLoaded, remainingLength, - dataSpec.key, dataSpec.flags); - } - } - /** * Returns the integer equal to the big-endian concatenation of the characters in {@code string} * as bytes. The string must be no more than four characters long. diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 6997324f02..29b7e4a6a8 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -210,7 +210,7 @@ import java.util.concurrent.atomic.AtomicInteger; // According to spec, for packed audio, initDataSpec is expected to be null. return; } - DataSpec initSegmentDataSpec = Util.getRemainderDataSpec(initDataSpec, initSegmentBytesLoaded); + DataSpec initSegmentDataSpec = initDataSpec.subrange(initSegmentBytesLoaded); try { ExtractorInput input = new DefaultExtractorInput(initDataSource, initSegmentDataSpec.absoluteStreamPosition, initDataSource.open(initSegmentDataSpec)); @@ -239,7 +239,7 @@ import java.util.concurrent.atomic.AtomicInteger; loadDataSpec = dataSpec; skipLoadedBytes = bytesLoaded != 0; } else { - loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); + loadDataSpec = dataSpec.subrange(bytesLoaded); skipLoadedBytes = false; } if (!isMasterTimestampSource) { @@ -396,7 +396,7 @@ import java.util.concurrent.atomic.AtomicInteger; } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { extractor = new Mp3Extractor(0, startTimeUs); } else { - throw new IllegalArgumentException("Unkown extension for audio file: " + lastPathSegment); + throw new IllegalArgumentException("Unknown extension for audio file: " + lastPathSegment); } extractor.init(extractorOutput); return extractor; From ed060a8f3397253129224b96364718288e67942b Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 21 Jun 2017 03:54:00 -0700 Subject: [PATCH 147/220] Add FakeDataSource.setTestData() to simplify adding random test data ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159676653 --- .../exoplayer2/testutil/FakeDataSource.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java index db11cc98a3..4d42c3e48e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java @@ -343,26 +343,36 @@ public final class FakeDataSource implements DataSource { dataMap = new HashMap<>(); } + /** Sets the default data, overwrites if there is one already. */ public FakeData newDefaultData() { defaultData = new FakeData(this, null); return defaultData; } + /** Sets random data with the given {@code length} for the given {@code uri}. */ + public FakeDataSet setRandomData(String uri, int length) { + return setData(uri, TestUtil.buildTestData(length)); + } + + /** Sets the given {@code data} for the given {@code uri}. */ + public FakeDataSet setData(String uri, byte[] data) { + return newData(uri).appendReadData(data).endData(); + } + + /** Returns a new {@link FakeData} with the given {@code uri}. */ public FakeData newData(String uri) { FakeData data = new FakeData(this, uri); dataMap.put(uri, data); return data; } - public FakeDataSet setData(String uri, byte[] data) { - return newData(uri).appendReadData(data).endData(); - } - + /** Returns the data for the given {@code uri}, or {@code defaultData} if no data is set. */ public FakeData getData(String uri) { FakeData data = dataMap.get(uri); return data != null ? data : defaultData; } + /** Returns a list of all data including {@code defaultData}. */ public ArrayList getAllData() { ArrayList fakeDatas = new ArrayList<>(dataMap.values()); if (defaultData != null) { From 9154d54df9fe3d5e6699ab6c780f6148be406f64 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 21 Jun 2017 04:00:02 -0700 Subject: [PATCH 148/220] Fix resolveSubsequentPeriod in EPII. getNextPeriod might return C.INDEX_UNSET. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159676949 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 4 ++++ 1 file changed, 4 insertions(+) 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 bcfcc4451b..38b9648162 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 @@ -1083,6 +1083,10 @@ import java.io.IOException; int maxIterations = oldTimeline.getPeriodCount(); for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) { oldPeriodIndex = oldTimeline.getNextPeriodIndex(oldPeriodIndex, period, window, repeatMode); + if (oldPeriodIndex == C.INDEX_UNSET) { + // We've reached the end of the old timeline. + break; + } newPeriodIndex = newTimeline.getIndexOfPeriod( oldTimeline.getPeriod(oldPeriodIndex, period, true).uid); } From 59ccd63544caadd4a1facee3b7ce5fa08e18f493 Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 7 May 2017 05:19:29 +0100 Subject: [PATCH 149/220] Remove weird nextOffset state from SampleMetadataQueue ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159681714 --- .../exoplayer2/source/SampleMetadataQueue.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 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 f52d84ef69..a4ee3f2285 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 @@ -55,7 +55,6 @@ import com.google.android.exoplayer2.util.Util; private int relativeStartIndex; private int readPosition; - private long nextOffset; private long largestDequeuedTimestampUs; private long largestQueuedTimestampUs; private boolean upstreamKeyframeRequired; @@ -249,8 +248,6 @@ import com.google.android.exoplayer2.util.Util; largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, buffer.timeUs); readPosition++; - nextOffset = extrasHolder.offset + extrasHolder.size; - return C.RESULT_BUFFER_READ; } @@ -298,7 +295,6 @@ import com.google.android.exoplayer2.util.Util; } readPosition += sampleCountToKeyframe; - nextOffset = offsets[(relativeStartIndex + readPosition) % capacity]; return true; } @@ -310,8 +306,6 @@ import com.google.android.exoplayer2.util.Util; return; } readPosition = length; - int relativeLastSampleIndex = (relativeStartIndex + readPosition - 1) % capacity; - nextOffset = offsets[relativeLastSampleIndex] + sizes[relativeLastSampleIndex]; } /** @@ -331,7 +325,12 @@ import com.google.android.exoplayer2.util.Util; relativeStartIndex -= capacity; } readPosition = 0; - return length == 0 ? nextOffset : offsets[relativeStartIndex]; + if (length == 0) { + int relativeLastDiscardedIndex = (relativeStartIndex - 1 + capacity) % capacity; + return offsets[relativeLastDiscardedIndex] + sizes[relativeLastDiscardedIndex]; + } else { + return offsets[relativeStartIndex]; + } } // Called by the loading thread. From 8e49cab865c2ef02961ad1e03512fc1e63729510 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 21 Jun 2017 07:03:05 -0700 Subject: [PATCH 150/220] Start finalizing SampleQueue methods It's a bit messy at the moment with the deprecated methods in there, but on the read side the new set of methods is as follows: Modifies the start of buffer: - discardTo(time, keyframe, ...) [this is new] - discardToRead() - discardToEnd() Modifies the read position: - rewind() - advanceTo(time, keyframe, ...) [this is a generalization of skipToKeyframeBefore] - advanceToEnd() [previously called skipAll] - read(...) Which seems quite nice and self-consistent, and is powerful enough for everything that we need to do as we move MediaSource implementations over to the new methods. TODOs for subsequent changes: - Re-order methods in the two classes so that they're actually in the same order, and move the deprecated ones out of the way - Enhance SampleQueueTest to also cover new functionality, as we start transitioning MediaSource implementations over to use it. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159688660 --- .../exoplayer2/source/SampleQueueTest.java | 102 ++++++++--- .../source/SampleMetadataQueue.java | 166 ++++++++++++------ .../exoplayer2/source/SampleQueue.java | 99 +++++++---- 3 files changed, 256 insertions(+), 111 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 8723e39020..8f9bbbce79 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -56,9 +56,11 @@ public class SampleQueueTest extends TestCase { ALLOCATION_SIZE * 9, ALLOCATION_SIZE * 8 + 1, ALLOCATION_SIZE * 7, ALLOCATION_SIZE * 6 + 1, ALLOCATION_SIZE * 5, ALLOCATION_SIZE * 3, ALLOCATION_SIZE + 1, 0 }; - private static final int[] TEST_SAMPLE_TIMESTAMPS = new int[] { + private static final long[] TEST_SAMPLE_TIMESTAMPS = new long[] { 0, 1000, 2000, 3000, 4000, 5000, 6000, 7000 }; + private static final long LAST_SAMPLE_TIMESTAMP = + TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1]; private static final int[] TEST_SAMPLE_FLAGS = new int[] { C.BUFFER_FLAG_KEY_FRAME, 0, 0, 0, C.BUFFER_FLAG_KEY_FRAME, 0, 0, 0 }; @@ -173,8 +175,7 @@ public class SampleQueueTest extends TestCase { public void testReadMultiSamples() { writeTestData(); - assertEquals(TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1], - sampleQueue.getLargestQueuedTimestampUs()); + assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs()); assertAllocationCount(10); assertReadTestData(); assertAllocationCount(10); @@ -219,9 +220,9 @@ public class SampleQueueTest extends TestCase { assertReadEndOfStream(false); } - public void testSkipAll() { + public void testAdvanceToEnd() { writeTestData(); - sampleQueue.skipAll2(); + sampleQueue.advanceToEnd(); assertAllocationCount(10); sampleQueue.discardToRead(); assertAllocationCount(0); @@ -232,10 +233,10 @@ public class SampleQueueTest extends TestCase { assertNoSamplesToRead(TEST_FORMAT_2); } - public void testSkipAllRetainsUnassignedData() { + public void testAdvanceToEndRetainsUnassignedData() { sampleQueue.format(TEST_FORMAT_1); sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE); - sampleQueue.skipAll2(); + sampleQueue.advanceToEnd(); assertAllocationCount(1); sampleQueue.discardToRead(); // Skipping shouldn't discard data that may belong to a sample whose metadata has yet to be @@ -255,54 +256,107 @@ public class SampleQueueTest extends TestCase { assertAllocationCount(0); } - public void testSkipToKeyframeBeforeBuffer() { + public void testAdvanceToBeforeBuffer() { writeTestData(); - boolean result = sampleQueue.skipToKeyframeBefore2(TEST_SAMPLE_TIMESTAMPS[0] - 1, false); + boolean result = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0] - 1, true, false); // Should fail and have no effect. assertFalse(result); assertReadTestData(); assertNoSamplesToRead(TEST_FORMAT_2); } - public void testSkipToKeyframeStartOfBuffer() { + public void testAdvanceToStartOfBuffer() { writeTestData(); - boolean result = sampleQueue.skipToKeyframeBefore2(TEST_SAMPLE_TIMESTAMPS[0], false); + boolean result = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0], true, false); // Should succeed but have no effect (we're already at the first frame). assertTrue(result); assertReadTestData(); assertNoSamplesToRead(TEST_FORMAT_2); } - public void testSkipToKeyframeEndOfBuffer() { + public void testAdvanceToEndOfBuffer() { writeTestData(); - boolean result = sampleQueue.skipToKeyframeBefore2( - TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1], false); + boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false); // Should succeed and skip to 2nd keyframe. assertTrue(result); assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX); assertNoSamplesToRead(TEST_FORMAT_2); } - public void testSkipToKeyframeAfterBuffer() { + public void testAdvanceToAfterBuffer() { writeTestData(); - boolean result = sampleQueue.skipToKeyframeBefore2( - TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1] + 1, false); + boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, false); // Should fail and have no effect. assertFalse(result); assertReadTestData(); assertNoSamplesToRead(TEST_FORMAT_2); } - public void testSkipToKeyframeAfterBufferAllowed() { + public void testAdvanceToAfterBufferAllowed() { writeTestData(); - boolean result = sampleQueue.skipToKeyframeBefore2( - TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1] + 1, true); + boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true); // Should succeed and skip to 2nd keyframe. assertTrue(result); assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX); assertNoSamplesToRead(TEST_FORMAT_2); } + public void testDiscardToEnd() { + writeTestData(); + // Should discard everything. + sampleQueue.discardToEnd(); + assertEquals(8, sampleQueue.getReadIndex()); + assertAllocationCount(0); + // We should still be able to read the upstream format. + assertReadFormat(false, TEST_FORMAT_2); + // We should be able to write and read subsequent samples. + writeTestData(); + assertReadTestData(TEST_FORMAT_2); + } + + public void testDiscardToStopAtReadPosition() { + writeTestData(); + // Shouldn't discard anything. + sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true); + assertEquals(0, sampleQueue.getReadIndex()); + assertAllocationCount(10); + // Read the first sample. + assertReadTestData(null, 0, 1); + // Shouldn't discard anything. + sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1] - 1, false, true); + assertEquals(1, sampleQueue.getReadIndex()); + assertAllocationCount(10); + // Should discard the read sample. + sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1], false, true); + assertAllocationCount(9); + // Shouldn't discard anything. + sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true); + assertAllocationCount(9); + // Should be able to read the remaining samples. + assertReadTestData(TEST_FORMAT_1, 1, 7); + assertEquals(8, sampleQueue.getReadIndex()); + // Should discard up to the second last sample + sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP - 1, false, true); + assertAllocationCount(3); + // Should discard up the last sample + sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true); + assertAllocationCount(1); + } + + public void testDiscardToDontStopAtReadPosition() { + writeTestData(); + // Shouldn't discard anything. + sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1] - 1, false, false); + assertEquals(0, sampleQueue.getReadIndex()); + assertAllocationCount(10); + // Should discard the first sample. + sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1], false, false); + assertEquals(1, sampleQueue.getReadIndex()); + assertAllocationCount(9); + // Should be able to read the remaining samples. + assertReadTestData(TEST_FORMAT_1, 1, 7); + } + public void testDiscardUpstream() { writeTestData(); sampleQueue.discardUpstreamSamples(8); @@ -476,7 +530,7 @@ public class SampleQueueTest extends TestCase { */ private void assertReadNothing(boolean formatRequired) { clearFormatHolderAndInputBuffer(); - int result = sampleQueue.readData2(formatHolder, inputBuffer, formatRequired, false, 0); + int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0); assertEquals(C.RESULT_NOTHING_READ, result); // formatHolder should not be populated. assertNull(formatHolder.format); @@ -493,7 +547,7 @@ public class SampleQueueTest extends TestCase { */ private void assertReadEndOfStream(boolean formatRequired) { clearFormatHolderAndInputBuffer(); - int result = sampleQueue.readData2(formatHolder, inputBuffer, formatRequired, true, 0); + int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, true, 0); assertEquals(C.RESULT_BUFFER_READ, result); // formatHolder should not be populated. assertNull(formatHolder.format); @@ -513,7 +567,7 @@ public class SampleQueueTest extends TestCase { */ private void assertReadFormat(boolean formatRequired, Format format) { clearFormatHolderAndInputBuffer(); - int result = sampleQueue.readData2(formatHolder, inputBuffer, formatRequired, false, 0); + int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0); assertEquals(C.RESULT_FORMAT_READ, result); // formatHolder should be populated. assertEquals(format, formatHolder.format); @@ -535,7 +589,7 @@ public class SampleQueueTest extends TestCase { private void assertSampleRead(long timeUs, boolean isKeyframe, byte[] sampleData, int offset, int length) { clearFormatHolderAndInputBuffer(); - int result = sampleQueue.readData2(formatHolder, inputBuffer, false, false, 0); + int result = sampleQueue.read(formatHolder, inputBuffer, false, false, 0); assertEquals(C.RESULT_BUFFER_READ, result); // formatHolder should not be populated. assertNull(formatHolder.format); 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 a4ee3f2285..5a69222251 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 @@ -214,7 +214,7 @@ import com.google.android.exoplayer2.util.Util; * or {@link C#RESULT_BUFFER_READ}. */ @SuppressWarnings("ReferenceEquality") - public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + public synchronized int read(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, boolean loadingFinished, Format downstreamFormat, SampleExtrasHolder extrasHolder) { if (!hasNextSample()) { @@ -252,56 +252,36 @@ import com.google.android.exoplayer2.util.Util; } /** - * Attempts to advance the read position to the keyframe before or at the specified time. If - * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs} - * falls within the buffer. + * Attempts to advance the read position to the sample before or at the specified time. * - * @param timeUs The seek time. - * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end - * of the buffer. - * @return Whether the read position was advanced to the keyframe before or at the specified time. + * @param timeUs The time to advance to. + * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified + * time, rather than to any sample before or at that time. + * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the + * end of the buffer, by advancing the read position to the last sample (or keyframe) in the + * buffer. + * @return Whether the operation was a success. A successful advance is one in which the read + * position was unchanged or advanced, and is now at a sample meeting the specified criteria. */ - public synchronized boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) { + public synchronized boolean advanceTo(long timeUs, boolean toKeyframe, + boolean allowTimeBeyondBuffer) { int relativeReadIndex = (relativeStartIndex + readPosition) % capacity; - if (!hasNextSample() || timeUs < timesUs[relativeReadIndex]) { + if (!hasNextSample() || timeUs < timesUs[relativeReadIndex] + || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) { return false; } - - if (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer) { + int offset = findSampleBefore(relativeReadIndex, length - readPosition, timeUs, toKeyframe); + if (offset == -1) { return false; } - - // This could be optimized to use a binary search, however in practice callers to this method - // often pass times near to the start of the buffer. Hence it's unclear whether switching to - // a binary search would yield any real benefit. - int sampleCount = 0; - int sampleCountToKeyframe = -1; - int searchIndex = relativeReadIndex; - int relativeEndIndex = (relativeStartIndex + length) % capacity; - while (searchIndex != relativeEndIndex) { - if (timesUs[searchIndex] > timeUs) { - // We've gone too far. - break; - } else if ((flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { - // We've found a keyframe, and we're still before the seek position. - sampleCountToKeyframe = sampleCount; - } - searchIndex = (searchIndex + 1) % capacity; - sampleCount++; - } - - if (sampleCountToKeyframe == -1) { - return false; - } - - readPosition += sampleCountToKeyframe; + readPosition += offset; return true; } /** - * Skips all samples in the buffer. + * Advances the read position to the end of the queue. */ - public synchronized void skipAll() { + public synchronized void advanceToEnd() { if (!hasNextSample()) { return; } @@ -309,28 +289,53 @@ import com.google.android.exoplayer2.util.Util; } /** - * Discards all samples in the buffer prior to the read position. + * Discards up to but not including the sample immediately before or at the specified time. * - * @return The offset up to which data should be dropped, or {@link C#POSITION_UNSET} if no - * dropping of data is required. + * @param timeUs The time to discard up to. + * @param toKeyframe If true then discards samples up to the keyframe before or at the specified + * time, rather than just any sample before or at that time. + * @param stopAtReadPosition If true then samples are only discarded if they're before the read + * position. If false then samples at and beyond the read position may be discarded, in which + * case the read position is advanced to the first remaining sample. + * @return The corresponding offset up to which data should be discarded, or + * {@link C#POSITION_UNSET} if no discarding of data is necessary. + */ + public synchronized long discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { + if (length == 0 || timeUs < timesUs[relativeStartIndex]) { + return C.POSITION_UNSET; + } + int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length; + int discardCount = findSampleBefore(relativeStartIndex, searchLength, timeUs, toKeyframe); + if (discardCount == -1) { + return C.POSITION_UNSET; + } + return discardSamples(discardCount); + } + + /** + * Discards samples up to but not including the read position. + * + * @return The corresponding offset up to which data should be discarded, or + * {@link C#POSITION_UNSET} if no discarding of data is necessary. */ public synchronized long discardToRead() { if (readPosition == 0) { return C.POSITION_UNSET; } - length -= readPosition; - absoluteStartIndex += readPosition; - relativeStartIndex += readPosition; - if (relativeStartIndex >= capacity) { - relativeStartIndex -= capacity; - } - readPosition = 0; + return discardSamples(readPosition); + } + + /** + * Discards all samples in the queue. The read position is also advanced. + * + * @return The corresponding offset up to which data should be discarded, or + * {@link C#POSITION_UNSET} if no discarding of data is necessary. + */ + public synchronized long discardToEnd() { if (length == 0) { - int relativeLastDiscardedIndex = (relativeStartIndex - 1 + capacity) % capacity; - return offsets[relativeLastDiscardedIndex] + sizes[relativeLastDiscardedIndex]; - } else { - return offsets[relativeStartIndex]; + return C.POSITION_UNSET; } + return discardSamples(length); } // Called by the loading thread. @@ -434,4 +439,59 @@ import com.google.android.exoplayer2.util.Util; return true; } + // Internal methods. + + /** + * Finds the sample in the specified range that's before or at the specified time. If + * {@code keyframe} is {@code true} then the sample is additionally required to be a keyframe. + * + * @param relativeStartIndex The relative index from which to start searching. + * @param length The length of the range being searched. + * @param timeUs The specified time. + * @param keyframe Whether only keyframes should be considered. + * @return The offset from {@code relativeStartIndex} to the found sample, or -1 if no matching + * sample was found. + */ + private int findSampleBefore(int relativeStartIndex, int length, long timeUs, boolean keyframe) { + // This could be optimized to use a binary search, however in practice callers to this method + // normally pass times near to the start of the search region. Hence it's unclear whether + // switching to a binary search would yield any real benefit. + int sampleCountToTarget = -1; + int searchIndex = relativeStartIndex; + for (int i = 0; i < length && timesUs[searchIndex] <= timeUs; i++) { + if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + // We've found a suitable sample. + sampleCountToTarget = i; + } + searchIndex = (searchIndex + 1) % capacity; + } + return sampleCountToTarget; + } + + /** + * Discards the specified number of samples. + * + * @param discardCount The number of samples to discard. + * @return The corresponding offset up to which data should be discarded, or + * {@link C#POSITION_UNSET} if no discarding of data is necessary. + */ + private long discardSamples(int discardCount) { + length -= discardCount; + absoluteStartIndex += discardCount; + relativeStartIndex += discardCount; + if (relativeStartIndex >= capacity) { + relativeStartIndex -= capacity; + } + readPosition -= discardCount; + if (readPosition < 0) { + readPosition = 0; + } + if (length == 0) { + int relativeLastDiscardedIndex = (relativeStartIndex - 1 + capacity) % capacity; + return offsets[relativeLastDiscardedIndex] + sizes[relativeLastDiscardedIndex]; + } else { + return offsets[relativeStartIndex]; + } + } + } 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 35a53c5cdf..6e99ef7f2c 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 @@ -232,65 +232,84 @@ public final class SampleQueue implements TrackOutput { } /** - * Discards samples up to the current read position. + * Discards up to but not including the sample immediately before or at the specified time. + * + * @param timeUs The time to discard to. + * @param toKeyframe If true then discards samples up to the keyframe before or at the specified + * time, rather than any sample before or at that time. + * @param stopAtReadPosition If true then samples are only discarded if they're before the + * read position. If false then samples at and beyond the read position may be discarded, in + * which case the read position is advanced to the first remaining sample. */ - public void discardToRead() { - long nextOffset = metadataQueue.discardToRead(); - if (nextOffset != C.POSITION_UNSET) { - dropDownstreamTo(nextOffset); - } + public void discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { + discardDownstreamTo(metadataQueue.discardTo(timeUs, toKeyframe, stopAtReadPosition)); } /** - * @deprecated Use {@link #skipAll2()} followed by {@link #discardToRead()}. + * Discards up to but not including the read position. + */ + public void discardToRead() { + discardDownstreamTo(metadataQueue.discardToRead()); + } + + /** + * Discards to the end of the queue. The read position is also advanced. + */ + public void discardToEnd() { + discardDownstreamTo(metadataQueue.discardToEnd()); + } + + /** + * @deprecated Use {@link #advanceToEnd()} followed by {@link #discardToRead()}. */ @Deprecated public void skipAll() { - skipAll2(); + advanceToEnd(); discardToRead(); } /** - * Skips all samples currently in the buffer. + * Advances the read position to the end of the queue. */ - public void skipAll2() { - metadataQueue.skipAll(); + public void advanceToEnd() { + metadataQueue.advanceToEnd(); } /** - * @deprecated Use {@link #skipToKeyframeBefore2(long, boolean)} followed by + * @deprecated Use {@link #advanceTo(long, boolean, boolean)} followed by * {@link #discardToRead()}. */ @Deprecated public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) { - boolean success = skipToKeyframeBefore2(timeUs, allowTimeBeyondBuffer); + boolean success = advanceTo(timeUs, true, allowTimeBeyondBuffer); discardToRead(); return success; } /** - * Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer - * contains a keyframe with a timestamp of {@code timeUs} or earlier. If - * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs} - * falls within the buffer. + * Attempts to advance the read position to the sample before or at the specified time. * - * @param timeUs The seek time. - * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end - * of the buffer. - * @return Whether the skip was successful. + * @param timeUs The time to advance to. + * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified + * time, rather than to any sample before or at that time. + * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the + * end of the buffer, by advancing the read position to the last sample (or keyframe) in the + * buffer. + * @return Whether the operation was a success. A successful advance is one in which the read + * position was unchanged or advanced, and is now at a sample meeting the specified criteria. */ - public boolean skipToKeyframeBefore2(long timeUs, boolean allowTimeBeyondBuffer) { - return metadataQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer); + public boolean advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) { + return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer); } /** - * @deprecated Use {@link #readData2(FormatHolder, DecoderInputBuffer, boolean, boolean, long)} + * @deprecated Use {@link #read(FormatHolder, DecoderInputBuffer, boolean, boolean, long)} * followed by {@link #discardToRead()}. */ @Deprecated public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, boolean loadingFinished, long decodeOnlyUntilUs) { - int result = readData2(formatHolder, buffer, formatRequired, loadingFinished, + int result = read(formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilUs); discardToRead(); return result; @@ -312,9 +331,9 @@ public final class SampleQueue implements TrackOutput { * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or * {@link C#RESULT_BUFFER_READ}. */ - public int readData2(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, + public int read(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, boolean loadingFinished, long decodeOnlyUntilUs) { - int result = metadataQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, + int result = metadataQueue.read(formatHolder, buffer, formatRequired, loadingFinished, downstreamFormat, extrasHolder); switch (result) { case C.RESULT_FORMAT_READ: @@ -472,17 +491,27 @@ public final class SampleQueue implements TrackOutput { } /** - * Advances {@link #firstAllocationNode} to the specified absolute position. Nodes that are - * advanced over are cleared, and their underlying allocations are returned to the allocator. + * Advances {@link #firstAllocationNode} to the specified absolute position. + * {@link #readAllocationNode} is also advanced if necessary to avoid it falling behind + * {@link #firstAllocationNode}. Nodes that have been advanced past are cleared, and their + * underlying allocations are returned to the allocator. * * @param absolutePosition The position to which {@link #firstAllocationNode} should be advanced. - * Must never exceed the absolute position of the next sample to be read. + * May be {@link C#POSITION_UNSET}, in which case calling this method is a no-op. */ - private void dropDownstreamTo(long absolutePosition) { + private void discardDownstreamTo(long absolutePosition) { + if (absolutePosition == C.POSITION_UNSET) { + return; + } while (absolutePosition >= firstAllocationNode.endPosition) { allocator.release(firstAllocationNode.allocation); firstAllocationNode = firstAllocationNode.clear(); } + // If we discarded the node referenced by readAllocationNode then we need to advance it to the + // first remaining node. + if (readAllocationNode.startPosition < firstAllocationNode.startPosition) { + readAllocationNode = firstAllocationNode; + } } // Called by the loading thread. @@ -742,13 +771,15 @@ public final class SampleQueue implements TrackOutput { } /** - * Clears {@link #allocation}. + * Clears {@link #allocation} and {@link #next}. * - * @return The next {@link AllocationNode}, for convenience. + * @return The cleared next {@link AllocationNode}. */ public AllocationNode clear() { allocation = null; - return next; + AllocationNode temp = next; + next = null; + return temp; } } From 531eb15ff46522b51aa1936c17f821bed4130b16 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 21 Jun 2017 07:04:59 -0700 Subject: [PATCH 151/220] Move DashDownloadTestBase assert methods to CacheAsserts CacheAsserts contains cache assertion methods for testing. It's easier to use in tests than DashDownloadTestBase which requires to be extended. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159688808 --- .../upstream/cache/CacheDataSourceTest.java | 38 ++++--- .../exoplayer2/testutil/CacheAsserts.java | 106 ++++++++++++++++++ 2 files changed, 127 insertions(+), 17 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index 5c342ae3d3..ca7d5d6214 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream.cache; +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty; + import android.net.Uri; import android.test.InstrumentationTestCase; import android.test.MoreAsserts; @@ -38,27 +40,29 @@ public class CacheDataSourceTest extends InstrumentationTestCase { private static final String KEY_1 = "key 1"; private static final String KEY_2 = "key 2"; - private File cacheDir; - private SimpleCache simpleCache; + private File tempFolder; + private SimpleCache cache; @Override - protected void setUp() throws Exception { - cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); - simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); + public void setUp() throws Exception { + super.setUp(); + tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); } @Override - protected void tearDown() throws Exception { - Util.recursiveDelete(cacheDir); + public void tearDown() throws Exception { + Util.recursiveDelete(tempFolder); + super.tearDown(); } public void testMaxCacheFileSize() throws Exception { CacheDataSource cacheDataSource = createCacheDataSource(false, false); assertReadDataContentLength(cacheDataSource, false, false); - File[] files = cacheDir.listFiles(); - for (File file : files) { - if (!file.getName().equals(CachedContentIndex.FILE_NAME)) { - assertTrue(file.length() <= MAX_CACHE_FILE_SIZE); + for (String key : cache.getKeys()) { + for (CacheSpan cacheSpan : cache.getCachedSpans(key)) { + assertTrue(cacheSpan.length <= MAX_CACHE_FILE_SIZE); + assertTrue(cacheSpan.file.length() <= MAX_CACHE_FILE_SIZE); } } } @@ -104,7 +108,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase { // Read partial at EOS but don't cross it so length is unknown CacheDataSource cacheDataSource = createCacheDataSource(false, true); assertReadData(cacheDataSource, true, TEST_DATA.length - 2, 2); - assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1)); + assertEquals(C.LENGTH_UNSET, cache.getContentLength(KEY_1)); // Now do an unbounded request for whole data. This will cause a bounded request from upstream. // End of data from upstream shouldn't be mixed up with EOS and cause length set wrong. @@ -124,13 +128,13 @@ public class CacheDataSourceTest extends InstrumentationTestCase { CacheDataSource cacheDataSource = createCacheDataSource(false, true, CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS); assertReadData(cacheDataSource, true, 0, C.LENGTH_UNSET); - MoreAsserts.assertEmpty(simpleCache.getKeys()); + MoreAsserts.assertEmpty(cache.getKeys()); } public void testReadOnlyCache() throws Exception { CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null); assertReadDataContentLength(cacheDataSource, false, false); - assertEquals(0, cacheDir.list().length); + assertCacheEmpty(cache); } private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength) @@ -155,7 +159,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase { assertReadData(cacheDataSource, unknownLength, 0, length); assertEquals("When the range specified, CacheDataSource doesn't reach EOS so shouldn't cache " + "content length", !unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length, - simpleCache.getContentLength(KEY_1)); + cache.getContentLength(KEY_1)); } private void assertReadData(CacheDataSource cacheDataSource, boolean unknownLength, int position, @@ -192,7 +196,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase { private CacheDataSource createCacheDataSource(boolean setReadException, boolean simulateUnknownLength, @CacheDataSource.Flags int flags) { return createCacheDataSource(setReadException, simulateUnknownLength, flags, - new CacheDataSink(simpleCache, MAX_CACHE_FILE_SIZE)); + new CacheDataSink(cache, MAX_CACHE_FILE_SIZE)); } private CacheDataSource createCacheDataSource(boolean setReadException, @@ -204,7 +208,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase { if (setReadException) { fakeData.appendReadError(new IOException("Shouldn't read from upstream")); } - return new CacheDataSource(simpleCache, upstream, new FileDataSource(), cacheWriteDataSink, + return new CacheDataSource(cache, upstream, new FileDataSource(), cacheWriteDataSink, flags, null); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java new file mode 100644 index 0000000000..3494998e04 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -0,0 +1,106 @@ +/* + * 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.testutil; + +import static junit.framework.Assert.assertEquals; + +import android.net.Uri; +import android.test.MoreAsserts; +import com.google.android.exoplayer2.testutil.FakeDataSource.FakeData; +import com.google.android.exoplayer2.testutil.FakeDataSource.FakeDataSet; +import com.google.android.exoplayer2.upstream.DataSourceInputStream; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheUtil; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import junit.framework.Assert; + +/** + * Assertion methods for {@link Cache}. + */ +public final class CacheAsserts { + + /** Asserts that the cache content is equal to the data in the {@code fakeDataSet}. */ + public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException { + int totalLength = 0; + for (FakeData fakeData : fakeDataSet.getAllData()) { + byte[] data = fakeData.getData(); + assertCachedData(cache, fakeData.uri, data); + totalLength += data.length; + } + assertEquals(totalLength, cache.getCacheSpace()); + } + + /** + * Asserts that the cache content for the given {@code uriStrings} are equal to the data in the + * {@code fakeDataSet}. + */ + public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, String... uriStrings) + throws IOException { + for (String uriString : uriStrings) { + assertCachedData(cache, uriString, fakeDataSet.getData(uriString).getData()); + } + } + + /** + * Asserts that the cache content for the given {@code uriString} is equal to the {@code + * expected}. + */ + public static void assertCachedData(Cache cache, String uriString, byte[] expected) + throws IOException { + CacheDataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, + new DataSpec(Uri.parse(uriString), DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)); + try { + inputStream.open(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } catch (IOException e) { + // Ignore + } finally { + inputStream.close(); + } + MoreAsserts.assertEquals("Cached data doesn't match expected for '" + uriString + "',", + expected, outputStream.toByteArray()); + } + + /** Asserts that there is no cache content for the given {@code uriStrings}. */ + public static void assertNoCachedData(Cache cache, String... uriStrings) { + for (String uriString : uriStrings) { + Assert.assertNull("There is cached data for '" + uriString + "',", + cache.getCachedSpans(CacheUtil.generateKey(Uri.parse(uriString)))); + } + } + + /** + * Asserts that the cache is empty. + * + * @param cache + */ + public static void assertCacheEmpty(Cache cache) { + assertEquals(0, cache.getCacheSpace()); + } + + private CacheAsserts() {} + +} From 9bad78dce6439fcf0b99dff79a9d47dde6f7b6fa Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 21 Jun 2017 09:51:26 -0700 Subject: [PATCH 152/220] Fix SampleMetadataQueue.getLargestQueuedTimestampUs This was broken prior to my recent changes, since largestDequeuedTimestampUs was only being updated in readData. It should have been updated in the skip methods. as well. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159704945 --- .../exoplayer2/source/SampleQueueTest.java | 63 ++++++++++++++++--- .../source/SampleMetadataQueue.java | 51 +++++++++------ 2 files changed, 89 insertions(+), 25 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 8f9bbbce79..f1d2b6bfdd 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -421,6 +421,43 @@ public class SampleQueueTest extends TestCase { assertNoSamplesToRead(TEST_FORMAT_2); } + public void testLargestQueuedTimestampWithDiscardUpstream() { + writeTestData(); + assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs()); + sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 1); + // Discarding from upstream should reduce the largest timestamp. + assertEquals(TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 2], + sampleQueue.getLargestQueuedTimestampUs()); + sampleQueue.discardUpstreamSamples(0); + // Discarding everything from upstream without reading should unset the largest timestamp. + assertEquals(Long.MIN_VALUE, sampleQueue.getLargestQueuedTimestampUs()); + } + + public void testLargestQueuedTimestampWithDiscardUpstreamDecodeOrder() { + long[] decodeOrderTimestamps = new long[] {0, 3000, 2000, 1000, 4000, 7000, 6000, 5000}; + writeTestData(TEST_DATA, TEST_SAMPLE_SIZES, TEST_SAMPLE_OFFSETS, decodeOrderTimestamps, + TEST_SAMPLE_FORMATS, TEST_SAMPLE_FLAGS); + assertEquals(7000, sampleQueue.getLargestQueuedTimestampUs()); + sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 2); + // Discarding the last two samples should not change the largest timestamp, due to the decode + // ordering of the timestamps. + assertEquals(7000, sampleQueue.getLargestQueuedTimestampUs()); + sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 3); + // Once a third sample is discarded, the largest timestamp should have changed. + assertEquals(4000, sampleQueue.getLargestQueuedTimestampUs()); + sampleQueue.discardUpstreamSamples(0); + // Discarding everything from upstream without reading should unset the largest timestamp. + assertEquals(Long.MIN_VALUE, sampleQueue.getLargestQueuedTimestampUs()); + } + + public void testLargestQueuedTimestampWithRead() { + writeTestData(); + assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs()); + assertReadTestData(); + // Reading everything should not reduce the largest timestamp. + assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs()); + } + // Internal methods. /** @@ -428,15 +465,27 @@ public class SampleQueueTest extends TestCase { */ @SuppressWarnings("ReferenceEquality") private void writeTestData() { - sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), TEST_DATA.length); + writeTestData(TEST_DATA, TEST_SAMPLE_SIZES, TEST_SAMPLE_OFFSETS, TEST_SAMPLE_TIMESTAMPS, + TEST_SAMPLE_FORMATS, TEST_SAMPLE_FLAGS); + } + + /** + * Writes the specified test data to {@code sampleQueue}. + * + * + */ + @SuppressWarnings("ReferenceEquality") + private void writeTestData(byte[] data, int[] sampleSizes, int[] sampleOffsets, + long[] sampleTimestamps, Format[] sampleFormats, int[] sampleFlags) { + sampleQueue.sampleData(new ParsableByteArray(data), data.length); Format format = null; - for (int i = 0; i < TEST_SAMPLE_TIMESTAMPS.length; i++) { - if (TEST_SAMPLE_FORMATS[i] != format) { - sampleQueue.format(TEST_SAMPLE_FORMATS[i]); - format = TEST_SAMPLE_FORMATS[i]; + for (int i = 0; i < sampleTimestamps.length; i++) { + if (sampleFormats[i] != format) { + sampleQueue.format(sampleFormats[i]); + format = sampleFormats[i]; } - sampleQueue.sampleMetadata(TEST_SAMPLE_TIMESTAMPS[i], TEST_SAMPLE_FLAGS[i], - TEST_SAMPLE_SIZES[i], TEST_SAMPLE_OFFSETS[i], null); + sampleQueue.sampleMetadata(sampleTimestamps[i], sampleFlags[i], sampleSizes[i], + sampleOffsets[i], null); } } 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 5a69222251..06dab6aa2e 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 @@ -55,7 +55,7 @@ import com.google.android.exoplayer2.util.Util; private int relativeStartIndex; private int readPosition; - private long largestDequeuedTimestampUs; + private long largestDiscardedTimestampUs; private long largestQueuedTimestampUs; private boolean upstreamKeyframeRequired; private boolean upstreamFormatRequired; @@ -71,7 +71,7 @@ import com.google.android.exoplayer2.util.Util; sizes = new int[capacity]; cryptoDatas = new CryptoData[capacity]; formats = new Format[capacity]; - largestDequeuedTimestampUs = Long.MIN_VALUE; + largestDiscardedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE; upstreamFormatRequired = true; upstreamKeyframeRequired = true; @@ -88,7 +88,7 @@ import com.google.android.exoplayer2.util.Util; // Called by the consuming thread, but only when there is no loading thread. public void resetLargestParsedTimestamps() { - largestDequeuedTimestampUs = Long.MIN_VALUE; + largestDiscardedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE; } @@ -121,16 +121,8 @@ import com.google.android.exoplayer2.util.Util; length -= discardCount; relativeEndIndex = (relativeEndIndex + capacity - discardCount) % capacity; - // Update the largest queued timestamp, assuming that the timestamps prior to a keyframe are - // always less than the timestamp of the keyframe itself, and of subsequent frames. - largestQueuedTimestampUs = Long.MIN_VALUE; - for (int i = length - 1; i >= 0; i--) { - int sampleIndex = (relativeStartIndex + i) % capacity; - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timesUs[sampleIndex]); - if ((flags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { - break; - } - } + largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, + getLargestTimestamp(relativeStartIndex, length)); return offsets[relativeEndIndex]; } @@ -182,7 +174,7 @@ import com.google.android.exoplayer2.util.Util; * samples have been queued. */ public synchronized long getLargestQueuedTimestampUs() { - return Math.max(largestDequeuedTimestampUs, largestQueuedTimestampUs); + return largestQueuedTimestampUs; } /** @@ -246,7 +238,6 @@ import com.google.android.exoplayer2.util.Util; extrasHolder.offset = offsets[relativeReadIndex]; extrasHolder.cryptoData = cryptoDatas[relativeReadIndex]; - largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, buffer.timeUs); readPosition++; return C.RESULT_BUFFER_READ; } @@ -420,14 +411,16 @@ import com.google.android.exoplayer2.util.Util; } /** - * Attempts to discard samples from the tail of the queue to allow samples starting from the - * specified timestamp to be spliced in. + * Attempts to discard samples from the end of the queue to allow samples starting from the + * specified timestamp to be spliced in. Samples will not be discarded prior to the read position. * * @param timeUs The timestamp at which the splice occurs. * @return Whether the splice was successful. */ public synchronized boolean attemptSplice(long timeUs) { - if (largestDequeuedTimestampUs >= timeUs) { + long largestReadTimestampUs = Math.max(largestDiscardedTimestampUs, + getLargestTimestamp(relativeStartIndex, readPosition)); + if (largestReadTimestampUs >= timeUs) { return false; } int retainCount = length; @@ -476,6 +469,8 @@ import com.google.android.exoplayer2.util.Util; * {@link C#POSITION_UNSET} if no discarding of data is necessary. */ private long discardSamples(int discardCount) { + largestDiscardedTimestampUs = Math.max(largestDiscardedTimestampUs, + getLargestTimestamp(relativeStartIndex, discardCount)); length -= discardCount; absoluteStartIndex += discardCount; relativeStartIndex += discardCount; @@ -494,4 +489,24 @@ import com.google.android.exoplayer2.util.Util; } } + /** + * Finds the largest timestamp in the specified range, assuming that the timestamps prior to a + * keyframe are always less than the timestamp of the keyframe itself, and of subsequent frames. + * + * @param relativeStartIndex The relative index from which to start searching. + * @param length The length of the range being searched. + * @return The largest timestamp, or {@link Long#MIN_VALUE} if {@code length <= 0}. + */ + private long getLargestTimestamp(int relativeStartIndex, int length) { + long largestTimestampUs = Long.MIN_VALUE; + for (int i = length - 1; i >= 0; i--) { + int sampleIndex = (relativeStartIndex + i) % capacity; + largestTimestampUs = Math.max(largestTimestampUs, timesUs[sampleIndex]); + if ((flags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + break; + } + } + return largestTimestampUs; + } + } From 73b17a7e7b96c280343638081febdce3179f0cf1 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 22 Jun 2017 02:46:42 -0700 Subject: [PATCH 153/220] Use HTTPS for all GCS URLs Also update the dizzy sample to use HTTPS as it has moved permanently. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159805004 --- demo/src/main/assets/media.exolist.json | 46 ++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/demo/src/main/assets/media.exolist.json b/demo/src/main/assets/media.exolist.json index 59ae87ad5f..8eaf8263a0 100644 --- a/demo/src/main/assets/media.exolist.json +++ b/demo/src/main/assets/media.exolist.json @@ -365,7 +365,7 @@ "samples": [ { "name": "Dizzy", - "uri": "http://html5demos.com/assets/dizzy.mp4" + "uri": "https://html5demos.com/assets/dizzy.mp4" }, { "name": "Apple AAC 10s", @@ -377,7 +377,7 @@ }, { "name": "Android screens (Matroska)", - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" }, { "name": "Big Buck Bunny (MP4 Video)", @@ -401,7 +401,7 @@ }, { "name": "Google Play (MP3 Audio)", - "uri": "http://storage.googleapis.com/exoplayer-test-media-0/play.mp3" + "uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3" }, { "name": "Google Play (Ogg/Vorbis Audio)", @@ -432,10 +432,10 @@ "name": "Cats -> Dogs", "playlist": [ { - "uri": "http://html5demos.com/assets/dizzy.mp4" + "uri": "https://html5demos.com/assets/dizzy.mp4" }, { - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" } ] }, @@ -446,7 +446,7 @@ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" }, { - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" }, { "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" @@ -459,13 +459,13 @@ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test", "playlist": [ { - "uri": "http://html5demos.com/assets/dizzy.mp4" + "uri": "https://html5demos.com/assets/dizzy.mp4" }, { "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" }, { - "uri": "http://html5demos.com/assets/dizzy.mp4" + "uri": "https://html5demos.com/assets/dizzy.mp4" }, { "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" @@ -482,77 +482,77 @@ "samples": [ { "name": "Single inline linear", - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" }, { "name": "Single skippable inline", - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dskippablelinear&correlator=" }, { "name": "Single redirect linear", - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirectlinear&correlator=" }, { "name": "Single redirect error", - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&nofb=1&correlator=" }, { "name": "Single redirect broken (fallback)", - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&correlator=" }, { "name": "VMAP pre-roll", - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonly&cmsid=496&vid=short_onecue&correlator=" }, { "name": "VMAP pre-roll + bumper", - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonlybumper&cmsid=496&vid=short_onecue&correlator=" }, { "name": "VMAP post-roll", - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonly&cmsid=496&vid=short_onecue&correlator=" }, { "name": "VMAP post-roll + bumper", - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonlybumper&cmsid=496&vid=short_onecue&correlator=" }, { "name": "VMAP pre-, mid- and post-rolls, single ads", - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" }, { "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad", - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpod&cmsid=496&vid=short_onecue&correlator=" }, { "name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad", - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpod&cmsid=496&vid=short_onecue&correlator=" }, { "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad (bumpers around all ad breaks)", - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpodbumper&cmsid=496&vid=short_onecue&correlator=" }, { "name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad (bumpers around all ad breaks)", - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpodbumper&cmsid=496&vid=short_onecue&correlator=" }, { "name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad", - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator=" } ] From aca80b8347796ba15913f78029e788ffa1e15082 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 22 Jun 2017 04:03:31 -0700 Subject: [PATCH 154/220] Add support for pattern encryption and default initialization vectors This will extend our CENC modes support to cbcs and cens. The change was not split into two different CLs due to lack of test content for default initialization vectors, aside from AES-CBCS encrypted ones. Issue:#1661 Issue:#1989 Issue:#2089 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159810371 --- demo/src/main/assets/media.exolist.json | 24 +++++++++ .../exoplayer2/decoder/CryptoInfo.java | 24 +++------ .../exoplayer2/extractor/TrackOutput.java | 28 +++++++++- .../extractor/mkv/MatroskaExtractor.java | 3 +- .../exoplayer2/extractor/mp4/AtomParsers.java | 27 ++++++++-- .../extractor/mp4/FragmentedMp4Extractor.java | 54 +++++++++++++------ .../extractor/mp4/TrackEncryptionBox.java | 19 ++++++- .../exoplayer2/source/SampleQueue.java | 3 +- .../source/smoothstreaming/SsMediaPeriod.java | 3 +- 9 files changed, 142 insertions(+), 43 deletions(-) diff --git a/demo/src/main/assets/media.exolist.json b/demo/src/main/assets/media.exolist.json index 8eaf8263a0..e0110df80b 100644 --- a/demo/src/main/assets/media.exolist.json +++ b/demo/src/main/assets/media.exolist.json @@ -184,6 +184,30 @@ "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure SD & HD (cbcs,MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure SD (cbcs,MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure HD (cbcs,MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_hd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure UHD (cbcs,MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" } ] }, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java index 0d143cdf49..ec17de8d74 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java @@ -52,11 +52,11 @@ public final class CryptoInfo { /** * @see android.media.MediaCodec.CryptoInfo.Pattern */ - public int patternBlocksToEncrypt; + public int encryptedBlocks; /** * @see android.media.MediaCodec.CryptoInfo.Pattern */ - public int patternBlocksToSkip; + public int clearBlocks; private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo; private final PatternHolderV24 patternHolder; @@ -70,28 +70,20 @@ public final class CryptoInfo { * @see android.media.MediaCodec.CryptoInfo#set(int, int[], int[], byte[], byte[], int) */ public void set(int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData, - byte[] key, byte[] iv, @C.CryptoMode int mode) { + byte[] key, byte[] iv, @C.CryptoMode int mode, int encryptedBlocks, int clearBlocks) { this.numSubSamples = numSubSamples; this.numBytesOfClearData = numBytesOfClearData; this.numBytesOfEncryptedData = numBytesOfEncryptedData; this.key = key; this.iv = iv; this.mode = mode; - patternBlocksToEncrypt = 0; - patternBlocksToSkip = 0; + this.encryptedBlocks = encryptedBlocks; + this.clearBlocks = clearBlocks; if (Util.SDK_INT >= 16) { updateFrameworkCryptoInfoV16(); } } - public void setPattern(int patternBlocksToEncrypt, int patternBlocksToSkip) { - this.patternBlocksToEncrypt = patternBlocksToEncrypt; - this.patternBlocksToSkip = patternBlocksToSkip; - if (Util.SDK_INT >= 24) { - patternHolder.set(patternBlocksToEncrypt, patternBlocksToSkip); - } - } - /** * Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance. *

        @@ -122,7 +114,7 @@ public final class CryptoInfo { frameworkCryptoInfo.iv = iv; frameworkCryptoInfo.mode = mode; if (Util.SDK_INT >= 24) { - patternHolder.set(patternBlocksToEncrypt, patternBlocksToSkip); + patternHolder.set(encryptedBlocks, clearBlocks); } } @@ -137,8 +129,8 @@ public final class CryptoInfo { pattern = new android.media.MediaCodec.CryptoInfo.Pattern(0, 0); } - private void set(int blocksToEncrypt, int blocksToSkip) { - pattern.set(blocksToEncrypt, blocksToSkip); + private void set(int encryptedBlocks, int clearBlocks) { + pattern.set(encryptedBlocks, clearBlocks); frameworkCryptoInfo.setPattern(pattern); } 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 2054854796..a12a0315a4 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 @@ -42,9 +42,30 @@ public interface TrackOutput { */ public final byte[] encryptionKey; - public CryptoData(@C.CryptoMode int cryptoMode, byte[] encryptionKey) { + /** + * The number of encrypted blocks in the encryption pattern, 0 if pattern encryption does not + * apply. + */ + public final int encryptedBlocks; + + /** + * The number of clear blocks in the encryption pattern, 0 if pattern encryption does not + * apply. + */ + public final int clearBlocks; + + /** + * @param cryptoMode See {@link #cryptoMode}. + * @param encryptionKey See {@link #encryptionKey}. + * @param encryptedBlocks See {@link #encryptedBlocks}. + * @param clearBlocks See {@link #clearBlocks}. + */ + public CryptoData(@C.CryptoMode int cryptoMode, byte[] encryptionKey, int encryptedBlocks, + int clearBlocks) { this.cryptoMode = cryptoMode; this.encryptionKey = encryptionKey; + this.encryptedBlocks = encryptedBlocks; + this.clearBlocks = clearBlocks; } @Override @@ -56,13 +77,16 @@ public interface TrackOutput { return false; } CryptoData other = (CryptoData) obj; - return cryptoMode == other.cryptoMode && Arrays.equals(encryptionKey, other.encryptionKey); + return cryptoMode == other.cryptoMode && encryptedBlocks == other.encryptedBlocks + && clearBlocks == other.clearBlocks && Arrays.equals(encryptionKey, other.encryptionKey); } @Override public int hashCode() { int result = cryptoMode; result = 31 * result + Arrays.hashCode(encryptionKey); + result = 31 * result + encryptedBlocks; + result = 31 * result + clearBlocks; return result; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 4c8ca177e0..9f438d0977 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -893,7 +893,8 @@ public final class MatroskaExtractor implements Extractor { case ID_CONTENT_ENCRYPTION_KEY_ID: byte[] encryptionKey = new byte[contentSize]; input.readFully(encryptionKey, 0, contentSize); - currentTrack.cryptoData = new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, encryptionKey); + currentTrack.cryptoData = new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, encryptionKey, + 0, 0); // We assume patternless AES-CTR. break; case ID_SIMPLE_BLOCK: case ID_BLOCK: 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 65a3d87b45..f63010924c 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 @@ -1105,13 +1105,30 @@ import java.util.List; int childAtomSize = parent.readInt(); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_tenc) { - parent.skipBytes(6); - boolean defaultIsEncrypted = parent.readUnsignedByte() == 1; - int defaultInitVectorSize = parent.readUnsignedByte(); + int fullAtom = parent.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + parent.skipBytes(1); // reserved = 0. + int defaultCryptByteBlock = 0; + int defaultSkipByteBlock = 0; + if (version == 0) { + parent.skipBytes(1); // reserved = 0. + } else /* version 1 or greater */ { + int patternByte = parent.readUnsignedByte(); + defaultCryptByteBlock = (patternByte & 0xF0) >> 4; + defaultSkipByteBlock = patternByte & 0x0F; + } + boolean defaultIsProtected = parent.readUnsignedByte() == 1; + int defaultPerSampleIvSize = parent.readUnsignedByte(); byte[] defaultKeyId = new byte[16]; parent.readBytes(defaultKeyId, 0, defaultKeyId.length); - return new TrackEncryptionBox(defaultIsEncrypted, schemeType, defaultInitVectorSize, - defaultKeyId); + byte[] constantIv = null; + if (defaultIsProtected && defaultPerSampleIvSize == 0) { + int constantIvSize = parent.readUnsignedByte(); + constantIv = new byte[constantIvSize]; + parent.readBytes(constantIv, 0, constantIvSize); + } + return new TrackEncryptionBox(defaultIsProtected, schemeType, defaultPerSampleIvSize, + defaultKeyId, defaultCryptByteBlock, defaultSkipByteBlock, constantIv); } childPosition += childAtomSize; } 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 cc3f315014..a756edf0a4 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 @@ -128,6 +128,7 @@ public final class FragmentedMp4Extractor implements Extractor { private final ParsableByteArray nalPrefix; private final ParsableByteArray nalBuffer; private final ParsableByteArray encryptionSignalByte; + private final ParsableByteArray defaultInitializationVector; // Adjusts sample timestamps. private final TimestampAdjuster timestampAdjuster; @@ -197,6 +198,7 @@ public final class FragmentedMp4Extractor implements Extractor { nalPrefix = new ParsableByteArray(5); nalBuffer = new ParsableByteArray(); encryptionSignalByte = new ParsableByteArray(1); + defaultInitializationVector = new ParsableByteArray(); extendedTypeScratch = new byte[16]; containerAtoms = new Stack<>(); pendingMetadataSampleInfos = new LinkedList<>(); @@ -879,9 +881,9 @@ public final class FragmentedMp4Extractor implements Extractor { return; } if (Atom.parseFullAtomVersion(sbgpFullAtom) == 1) { - sbgp.skipBytes(4); + sbgp.skipBytes(4); // default_length. } - if (sbgp.readInt() != 1) { + if (sbgp.readInt() != 1) { // entry_count. throw new ParserException("Entry count in sbgp != 1 (unsupported)."); } @@ -894,25 +896,35 @@ public final class FragmentedMp4Extractor implements Extractor { int sgpdVersion = Atom.parseFullAtomVersion(sgpdFullAtom); if (sgpdVersion == 1) { if (sgpd.readUnsignedInt() == 0) { - throw new ParserException("Variable length decription in sgpd found (unsupported)"); + throw new ParserException("Variable length description in sgpd found (unsupported)"); } } else if (sgpdVersion >= 2) { - sgpd.skipBytes(4); + sgpd.skipBytes(4); // default_sample_description_index. } - if (sgpd.readUnsignedInt() != 1) { + if (sgpd.readUnsignedInt() != 1) { // entry_count. throw new ParserException("Entry count in sgpd != 1 (unsupported)."); } // CencSampleEncryptionInformationGroupEntry - sgpd.skipBytes(2); + sgpd.skipBytes(1); // reserved = 0. + int patternByte = sgpd.readUnsignedByte(); + int cryptByteBlock = (patternByte & 0xF0) >> 4; + int skipByteBlock = patternByte & 0x0F; boolean isProtected = sgpd.readUnsignedByte() == 1; if (!isProtected) { return; } - int initVectorSize = sgpd.readUnsignedByte(); + int perSampleIvSize = sgpd.readUnsignedByte(); byte[] keyId = new byte[16]; sgpd.readBytes(keyId, 0, keyId.length); + byte[] constantIv = null; + if (isProtected && perSampleIvSize == 0) { + int constantIvSize = sgpd.readUnsignedByte(); + constantIv = new byte[constantIvSize]; + sgpd.readBytes(constantIv, 0, constantIvSize); + } out.definesEncryptionData = true; - out.trackEncryptionBox = new TrackEncryptionBox(isProtected, schemeType, initVectorSize, keyId); + out.trackEncryptionBox = new TrackEncryptionBox(isProtected, schemeType, perSampleIvSize, keyId, + cryptByteBlock, skipByteBlock, constantIv); } /** @@ -1197,12 +1209,24 @@ public final class FragmentedMp4Extractor implements Extractor { */ private int appendSampleEncryptionData(TrackBundle trackBundle) { TrackFragment trackFragment = trackBundle.fragment; - ParsableByteArray sampleEncryptionData = trackFragment.sampleEncryptionData; int sampleDescriptionIndex = trackFragment.header.sampleDescriptionIndex; TrackEncryptionBox encryptionBox = trackFragment.trackEncryptionBox != null ? trackFragment.trackEncryptionBox : trackBundle.track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex); - int vectorSize = encryptionBox.initializationVectorSize; + + ParsableByteArray initializationVectorData; + int vectorSize; + if (encryptionBox.initializationVectorSize != 0) { + initializationVectorData = trackFragment.sampleEncryptionData; + vectorSize = encryptionBox.initializationVectorSize; + } else { + // The default initialization vector should be used. + byte[] initVectorData = encryptionBox.defaultInitializationVector; + defaultInitializationVector.reset(initVectorData, initVectorData.length); + initializationVectorData = defaultInitializationVector; + vectorSize = initVectorData.length; + } + boolean subsampleEncryption = trackFragment .sampleHasSubsampleEncryptionTable[trackBundle.currentSampleIndex]; @@ -1212,20 +1236,20 @@ public final class FragmentedMp4Extractor implements Extractor { TrackOutput output = trackBundle.output; output.sampleData(encryptionSignalByte, 1); // Write the vector. - output.sampleData(sampleEncryptionData, vectorSize); + output.sampleData(initializationVectorData, vectorSize); // If we don't have subsample encryption data, we're done. if (!subsampleEncryption) { return 1 + vectorSize; } // Write the subsample encryption data. - int subsampleCount = sampleEncryptionData.readUnsignedShort(); - sampleEncryptionData.skipBytes(-2); + ParsableByteArray subsampleEncryptionData = trackFragment.sampleEncryptionData; + int subsampleCount = subsampleEncryptionData.readUnsignedShort(); + subsampleEncryptionData.skipBytes(-2); int subsampleDataLength = 2 + 6 * subsampleCount; - output.sampleData(sampleEncryptionData, subsampleDataLength); + output.sampleData(subsampleEncryptionData, subsampleDataLength); return 1 + vectorSize + subsampleDataLength; } - /** Returns DrmInitData from leaf atoms. */ private static DrmInitData getDrmInitDataFromAtoms(List leafChildren) { ArrayList schemeDatas = null; 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 6f33d2222f..b987dad7fb 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 @@ -19,6 +19,7 @@ import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Assertions; /** * Encapsulates information parsed from a track encryption (tenc) box or sample group description @@ -49,19 +50,31 @@ public final class TrackEncryptionBox { */ public final int initializationVectorSize; + /** + * If {@link #initializationVectorSize} 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; /** * @param isEncrypted See {@link #isEncrypted}. * @param schemeType See {@link #schemeType}. * @param initializationVectorSize See {@link #initializationVectorSize}. * @param keyId See {@link TrackOutput.CryptoData#encryptionKey}. + * @param defaultEncryptedBlocks See {@link TrackOutput.CryptoData#encryptedBlocks}. + * @param defaultClearBlocks See {@link TrackOutput.CryptoData#clearBlocks}. + * @param defaultInitializationVector See {@link #defaultInitializationVector}. */ public TrackEncryptionBox(boolean isEncrypted, @Nullable String schemeType, - int initializationVectorSize, byte[] keyId) { + int initializationVectorSize, byte[] keyId, int defaultEncryptedBlocks, + int defaultClearBlocks, @Nullable byte[] defaultInitializationVector) { + Assertions.checkArgument(initializationVectorSize == 0 ^ defaultInitializationVector == null); this.isEncrypted = isEncrypted; this.schemeType = schemeType; this.initializationVectorSize = initializationVectorSize; - cryptoData = new TrackOutput.CryptoData(schemeToCryptoMode(schemeType), keyId); + this.defaultInitializationVector = defaultInitializationVector; + cryptoData = new TrackOutput.CryptoData(schemeToCryptoMode(schemeType), keyId, + defaultEncryptedBlocks, defaultClearBlocks); } @C.CryptoMode @@ -72,8 +85,10 @@ public final class TrackEncryptionBox { } switch (schemeType) { case "cenc": + case "cens": return C.CRYPTO_MODE_AES_CTR; case "cbc1": + case "cbcs": return C.CRYPTO_MODE_AES_CBC; default: Log.w(TAG, "Unsupported protection scheme type '" + schemeType + "'. Assuming AES-CTR " 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 6e99ef7f2c..86a92b5adc 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 @@ -426,7 +426,8 @@ public final class SampleQueue implements TrackOutput { // Populate the cryptoInfo. CryptoData cryptoData = extrasHolder.cryptoData; buffer.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes, - cryptoData.encryptionKey, buffer.cryptoInfo.iv, cryptoData.cryptoMode); + cryptoData.encryptionKey, buffer.cryptoInfo.iv, cryptoData.cryptoMode, + cryptoData.encryptedBlocks, cryptoData.clearBlocks); // Adjust the offset and size to take into account the bytes read. int bytesRead = (int) (offset - extrasHolder.offset); 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 8322da2471..a52c56aafd 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 @@ -68,8 +68,9 @@ import java.util.ArrayList; ProtectionElement protectionElement = manifest.protectionElement; if (protectionElement != null) { byte[] keyId = getProtectionElementKeyId(protectionElement.data); + // We assume pattern encryption does not apply. trackEncryptionBoxes = new TrackEncryptionBox[] { - new TrackEncryptionBox(true, null, INITIALIZATION_VECTOR_SIZE, keyId)}; + new TrackEncryptionBox(true, null, INITIALIZATION_VECTOR_SIZE, keyId, 0, 0, null)}; } else { trackEncryptionBoxes = null; } From 950c2159b00a2a0459de22569909bee6690f7ea9 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 22 Jun 2017 05:41:02 -0700 Subject: [PATCH 155/220] Discard upstream allocations more aggressively + doc cleanup - If we have garbage and discard , throw away the garbage too. - Cleanup some documentation to consistently refer to the queue as "queue" rather than "buffer". ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159816309 --- .../exoplayer2/source/SampleQueueTest.java | 12 ++-- .../source/SampleMetadataQueue.java | 56 +++++++++---------- .../exoplayer2/source/SampleQueue.java | 16 +++--- 3 files changed, 40 insertions(+), 44 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java index f1d2b6bfdd..89a3db3599 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -364,7 +364,7 @@ public class SampleQueueTest extends TestCase { sampleQueue.discardUpstreamSamples(7); assertAllocationCount(9); sampleQueue.discardUpstreamSamples(6); - assertAllocationCount(8); // Byte not belonging to sample prevents 7. + assertAllocationCount(7); sampleQueue.discardUpstreamSamples(5); assertAllocationCount(5); sampleQueue.discardUpstreamSamples(4); @@ -372,11 +372,11 @@ public class SampleQueueTest extends TestCase { sampleQueue.discardUpstreamSamples(3); assertAllocationCount(3); sampleQueue.discardUpstreamSamples(2); - assertAllocationCount(3); // Byte not belonging to sample prevents 2. + assertAllocationCount(2); sampleQueue.discardUpstreamSamples(1); - assertAllocationCount(2); // Byte not belonging to sample prevents 1. + assertAllocationCount(1); sampleQueue.discardUpstreamSamples(0); - assertAllocationCount(1); // Byte not belonging to sample prevents 0. + assertAllocationCount(0); assertReadFormat(false, TEST_FORMAT_2); assertNoSamplesToRead(TEST_FORMAT_2); } @@ -386,7 +386,7 @@ public class SampleQueueTest extends TestCase { sampleQueue.discardUpstreamSamples(4); assertAllocationCount(4); sampleQueue.discardUpstreamSamples(0); - assertAllocationCount(1); // Byte not belonging to sample prevents 0. + assertAllocationCount(0); assertReadFormat(false, TEST_FORMAT_2); assertNoSamplesToRead(TEST_FORMAT_2); } @@ -410,7 +410,7 @@ public class SampleQueueTest extends TestCase { sampleQueue.discardUpstreamSamples(7); assertAllocationCount(6); sampleQueue.discardUpstreamSamples(6); - assertAllocationCount(5); // Byte not belonging to sample prevents 4. + assertAllocationCount(4); sampleQueue.discardUpstreamSamples(5); assertAllocationCount(2); sampleQueue.discardUpstreamSamples(4); 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 06dab6aa2e..b782b25371 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 @@ -100,30 +100,23 @@ import com.google.android.exoplayer2.util.Util; } /** - * Discards samples from the write side of the buffer. + * Discards samples from the write side of the queue. * * @param discardFromIndex The absolute index of the first sample to be discarded. - * @return The reduced total number of bytes written, after the samples have been discarded. + * @return The reduced total number of bytes written after the samples have been discarded, or 0 + * if the queue is now empty. */ public long discardUpstreamSamples(int discardFromIndex) { int discardCount = getWriteIndex() - discardFromIndex; Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition)); - - int relativeEndIndex = (relativeStartIndex + length) % capacity; - if (discardCount == 0) { - if (absoluteStartIndex == 0 && length == 0) { - // Nothing has been written to the queue. - return 0; - } - int lastWriteIndex = (relativeEndIndex == 0 ? capacity : relativeEndIndex) - 1; - return offsets[lastWriteIndex] + sizes[lastWriteIndex]; - } - length -= discardCount; - relativeEndIndex = (relativeEndIndex + capacity - discardCount) % capacity; - largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, - getLargestTimestamp(relativeStartIndex, length)); - return offsets[relativeEndIndex]; + largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length)); + if (length == 0) { + return 0; + } else { + int relativeLastWriteIndex = (relativeStartIndex + length - 1) % capacity; + return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex]; + } } public void sourceId(int sourceId) { @@ -140,8 +133,10 @@ import com.google.android.exoplayer2.util.Util; } /** - * Peeks the source id of the next sample, or the current upstream source id if - * {@link #hasNextSample()} is {@code false}. + * Peeks the source id of the next sample to be read, or the current upstream source id if the + * queue is empty or if the read position is at the end of the queue. + * + * @return The source id. */ public int peekSourceId() { int relativeReadIndex = (relativeStartIndex + readPosition) % capacity; @@ -249,8 +244,8 @@ import com.google.android.exoplayer2.util.Util; * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified * time, rather than to any sample before or at that time. * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the - * end of the buffer, by advancing the read position to the last sample (or keyframe) in the - * buffer. + * end of the queue, by advancing the read position to the last sample (or keyframe) in the + * queue. * @return Whether the operation was a success. A successful advance is one in which the read * position was unchanged or advanced, and is now at a sample meeting the specified criteria. */ @@ -418,8 +413,11 @@ import com.google.android.exoplayer2.util.Util; * @return Whether the splice was successful. */ public synchronized boolean attemptSplice(long timeUs) { + if (length == 0) { + return timeUs > largestDiscardedTimestampUs; + } long largestReadTimestampUs = Math.max(largestDiscardedTimestampUs, - getLargestTimestamp(relativeStartIndex, readPosition)); + getLargestTimestamp(readPosition)); if (largestReadTimestampUs >= timeUs) { return false; } @@ -470,7 +468,7 @@ import com.google.android.exoplayer2.util.Util; */ private long discardSamples(int discardCount) { largestDiscardedTimestampUs = Math.max(largestDiscardedTimestampUs, - getLargestTimestamp(relativeStartIndex, discardCount)); + getLargestTimestamp(discardCount)); length -= discardCount; absoluteStartIndex += discardCount; relativeStartIndex += discardCount; @@ -482,22 +480,22 @@ import com.google.android.exoplayer2.util.Util; readPosition = 0; } if (length == 0) { - int relativeLastDiscardedIndex = (relativeStartIndex - 1 + capacity) % capacity; - return offsets[relativeLastDiscardedIndex] + sizes[relativeLastDiscardedIndex]; + int relativeLastDiscardIndex = (relativeStartIndex == 0 ? capacity : relativeStartIndex) - 1; + return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex]; } else { return offsets[relativeStartIndex]; } } /** - * Finds the largest timestamp in the specified range, assuming that the timestamps prior to a - * keyframe are always less than the timestamp of the keyframe itself, and of subsequent frames. + * Finds the largest timestamp of any sample from the start of the queue up to the specified + * length, assuming that the timestamps prior to a keyframe are always less than the timestamp of + * the keyframe itself, and of subsequent frames. * - * @param relativeStartIndex The relative index from which to start searching. * @param length The length of the range being searched. * @return The largest timestamp, or {@link Long#MIN_VALUE} if {@code length <= 0}. */ - private long getLargestTimestamp(int relativeStartIndex, int length) { + private long getLargestTimestamp(int length) { long largestTimestampUs = Long.MIN_VALUE; for (int i = length - 1; i >= 0; i--) { int sampleIndex = (relativeStartIndex + i) % capacity; 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 86a92b5adc..ad906bfe9d 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 @@ -120,8 +120,7 @@ public final class SampleQueue implements TrackOutput { } /** - * Indicates that samples subsequently queued to the buffer should be spliced into those already - * queued. + * Indicates samples that are subsequently queued should be spliced into those already queued. */ public void splice() { pendingSplice = true; @@ -135,14 +134,14 @@ public final class SampleQueue implements TrackOutput { } /** - * Discards samples from the write side of the buffer. + * Discards samples from the write side of the queue. * * @param discardFromIndex The absolute index of the first sample to be discarded. Must be in the * range [{@link #getReadIndex()}, {@link #getWriteIndex()}]. */ public void discardUpstreamSamples(int discardFromIndex) { totalBytesWritten = metadataQueue.discardUpstreamSamples(discardFromIndex); - if (totalBytesWritten == firstAllocationNode.startPosition) { + if (totalBytesWritten == 0 || totalBytesWritten == firstAllocationNode.startPosition) { clearAllocationNodes(firstAllocationNode); firstAllocationNode = new AllocationNode(totalBytesWritten, allocationLength); readAllocationNode = firstAllocationNode; @@ -193,8 +192,8 @@ public final class SampleQueue implements TrackOutput { } /** - * Peeks the source id of the next sample, or the current upstream source id if the buffer is - * empty. + * Peeks the source id of the next sample to be read, or the current upstream source id if the + * queue is empty or if the read position is at the end of the queue. * * @return The source id. */ @@ -293,8 +292,7 @@ public final class SampleQueue implements TrackOutput { * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified * time, rather than to any sample before or at that time. * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the - * end of the buffer, by advancing the read position to the last sample (or keyframe) in the - * buffer. + * end of the queue, by advancing the read position to the last sample (or keyframe). * @return Whether the operation was a success. A successful advance is one in which the read * position was unchanged or advanced, and is now at a sample meeting the specified criteria. */ @@ -528,7 +526,7 @@ public final class SampleQueue implements TrackOutput { /** * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples - * subsequently queued to the buffer. + * that are subsequently queued. * * @param sampleOffsetUs The timestamp offset in microseconds. */ From 499f9370a2ada8169f1df17cb7c18157c7d92b37 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 22 Jun 2017 05:49:09 -0700 Subject: [PATCH 156/220] Remove use of mod operator from SampleMetadataQueue It's no more complicated to avoid it, and according to the Art team it should be faster without. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159816746 --- .../source/SampleMetadataQueue.java | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 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 b782b25371..c9c44ab014 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 @@ -114,7 +114,7 @@ import com.google.android.exoplayer2.util.Util; if (length == 0) { return 0; } else { - int relativeLastWriteIndex = (relativeStartIndex + length - 1) % capacity; + int relativeLastWriteIndex = getRelativeIndex(length - 1); return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex]; } } @@ -139,7 +139,7 @@ import com.google.android.exoplayer2.util.Util; * @return The source id. */ public int peekSourceId() { - int relativeReadIndex = (relativeStartIndex + readPosition) % capacity; + int relativeReadIndex = getRelativeIndex(readPosition); return hasNextSample() ? sourceIds[relativeReadIndex] : upstreamSourceId; } @@ -217,7 +217,7 @@ import com.google.android.exoplayer2.util.Util; } } - int relativeReadIndex = (relativeStartIndex + readPosition) % capacity; + int relativeReadIndex = getRelativeIndex(readPosition); if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { formatHolder.format = formats[relativeReadIndex]; return C.RESULT_FORMAT_READ; @@ -251,7 +251,7 @@ import com.google.android.exoplayer2.util.Util; */ public synchronized boolean advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) { - int relativeReadIndex = (relativeStartIndex + readPosition) % capacity; + int relativeReadIndex = getRelativeIndex(readPosition); if (!hasNextSample() || timeUs < timesUs[relativeReadIndex] || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) { return false; @@ -352,7 +352,7 @@ import com.google.android.exoplayer2.util.Util; Assertions.checkState(!upstreamFormatRequired); commitSampleTimestamp(timeUs); - int relativeEndIndex = (relativeStartIndex + length) % capacity; + int relativeEndIndex = getRelativeIndex(length); timesUs[relativeEndIndex] = timeUs; offsets[relativeEndIndex] = offset; sizes[relativeEndIndex] = size; @@ -422,9 +422,13 @@ import com.google.android.exoplayer2.util.Util; return false; } int retainCount = length; - while (retainCount > readPosition - && timesUs[(relativeStartIndex + retainCount - 1) % capacity] >= timeUs) { + int relativeSampleIndex = getRelativeIndex(length - 1); + while (retainCount > readPosition && timesUs[relativeSampleIndex] >= timeUs) { retainCount--; + relativeSampleIndex--; + if (relativeSampleIndex == -1) { + relativeSampleIndex = capacity - 1; + } } discardUpstreamSamples(absoluteStartIndex + retainCount); return true; @@ -454,7 +458,10 @@ import com.google.android.exoplayer2.util.Util; // We've found a suitable sample. sampleCountToTarget = i; } - searchIndex = (searchIndex + 1) % capacity; + searchIndex++; + if (searchIndex == capacity) { + searchIndex = 0; + } } return sampleCountToTarget; } @@ -493,18 +500,35 @@ import com.google.android.exoplayer2.util.Util; * the keyframe itself, and of subsequent frames. * * @param length The length of the range being searched. - * @return The largest timestamp, or {@link Long#MIN_VALUE} if {@code length <= 0}. + * @return The largest timestamp, or {@link Long#MIN_VALUE} if {@code length == 0}. */ private long getLargestTimestamp(int length) { + if (length == 0) { + return Long.MIN_VALUE; + } long largestTimestampUs = Long.MIN_VALUE; - for (int i = length - 1; i >= 0; i--) { - int sampleIndex = (relativeStartIndex + i) % capacity; - largestTimestampUs = Math.max(largestTimestampUs, timesUs[sampleIndex]); - if ((flags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + int relativeSampleIndex = getRelativeIndex(length - 1); + for (int i = 0; i < length; i++) { + largestTimestampUs = Math.max(largestTimestampUs, timesUs[relativeSampleIndex]); + if ((flags[relativeSampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { break; } + relativeSampleIndex--; + if (relativeSampleIndex == -1) { + relativeSampleIndex = capacity - 1; + } } return largestTimestampUs; } + /** + * Returns the relative index for a given offset from the start of the queue. + * + * @param offset The offset, which must be in the range [0, length]. + */ + private int getRelativeIndex(int offset) { + int relativeIndex = relativeStartIndex + offset; + return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity; + } + } From 6cde8335ffdd91127f9745e7a4a016120ad442d7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 23 Jun 2017 05:09:01 -0700 Subject: [PATCH 157/220] Add setter method to child data holder of abstract concatenated timeline. Prevents that we forget to set variables. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159939180 --- .../source/AbstractConcatenatedTimeline.java | 18 +++++++++++++++++- .../source/ConcatenatingMediaSource.java | 6 ++---- .../exoplayer2/source/LoopingMediaSource.java | 6 ++---- 3 files changed, 21 insertions(+), 9 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 e54dce687b..714d72104b 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 @@ -28,7 +28,7 @@ import com.google.android.exoplayer2.Timeline; /** * Meta data of a child timeline. */ - protected static class ChildDataHolder { + protected static final class ChildDataHolder { /** * Child timeline. @@ -50,6 +50,22 @@ import com.google.android.exoplayer2.Timeline; */ public Object uid; + /** + * Set child holder data. + * + * @param timeline Child timeline. + * @param firstPeriodIndexInChild First period index belonging to the child timeline. + * @param firstWindowIndexInChild First window index belonging to the child timeline. + * @param uid UID of child timeline. + */ + public void setData(Timeline timeline, int firstPeriodIndexInChild, int firstWindowIndexInChild, + Object uid) { + this.timeline = timeline; + this.firstPeriodIndexInChild = firstPeriodIndexInChild; + this.firstWindowIndexInChild = firstWindowIndexInChild; + this.uid = uid; + } + } private final ChildDataHolder childDataHolder; 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 dc3b6cb1f5..347c6b77b8 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 @@ -231,10 +231,8 @@ public final class ConcatenatingMediaSource implements MediaSource { } private void getChildDataByChildIndex(int childIndex, ChildDataHolder childData) { - childData.timeline = timelines[childIndex]; - childData.firstPeriodIndexInChild = getFirstPeriodIndexInChild(childIndex); - childData.firstWindowIndexInChild = getFirstWindowIndexInChild(childIndex); - childData.uid = childIndex; + childData.setData(timelines[childIndex], getFirstPeriodIndexInChild(childIndex), + getFirstWindowIndexInChild(childIndex), childIndex); } private int getChildIndexByPeriodIndex(int periodIndex) { 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 c663142564..c5f4779217 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 @@ -141,10 +141,8 @@ public final class LoopingMediaSource implements MediaSource { } private void getChildDataByChildIndex(int childIndex, ChildDataHolder childData) { - childData.timeline = childTimeline; - childData.firstPeriodIndexInChild = childIndex * childPeriodCount; - childData.firstWindowIndexInChild = childIndex * childWindowCount; - childData.uid = childIndex; + childData.setData(childTimeline, childIndex * childPeriodCount, childIndex * childWindowCount, + childIndex); } } From 8bb643976fe20d1ec684291aa7bf5337e474bec4 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 23 Jun 2017 17:20:51 +0100 Subject: [PATCH 158/220] Fix ContentDataSource and enhance tests to validate read data --- .../core/src/androidTest/AndroidManifest.xml | 4 +- .../assets/binary/1024_incrementing_bytes.mp3 | Bin 0 -> 1024 bytes .../upstream/AndroidDataSourceConstants.java | 16 -- .../upstream/AssetDataSourceTest.java | 59 ++++++-- .../upstream/ContentDataSourceTest.java | 139 ++++++++++++++---- .../exoplayer2/upstream/TestDataProvider.java | 68 --------- .../upstream/ContentDataSource.java | 5 +- .../android/exoplayer2/testutil/TestUtil.java | 17 +++ 8 files changed, 185 insertions(+), 123 deletions(-) create mode 100644 library/core/src/androidTest/assets/binary/1024_incrementing_bytes.mp3 delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/TestDataProvider.java diff --git a/library/core/src/androidTest/AndroidManifest.xml b/library/core/src/androidTest/AndroidManifest.xml index 0a90b071d2..aeddc611cf 100644 --- a/library/core/src/androidTest/AndroidManifest.xml +++ b/library/core/src/androidTest/AndroidManifest.xml @@ -25,8 +25,8 @@ tools:ignore="MissingApplicationIcon,HardcodedDebugMode"> + android:authorities="com.google.android.exoplayer2.core.test" + android:name="com.google.android.exoplayer2.upstream.ContentDataSourceTest$TestContentProvider"/> MC+6cQE@6%&_`l#-T_m6KOcR8m$^Ra4i{)Y8_`)zddH zG%_|ZH8Z!cw6eCbwX=6{baHlab#wRd^z!!c_45x13RUz zF>}`JIdkXDU$Ah|;w4L$Enl&6)#^2C*R9{Mant54TeofBv2)k%J$v` Date: Fri, 23 Jun 2017 17:37:28 +0100 Subject: [PATCH 159/220] Mini cleanup --- .../exoplayer2/upstream/ContentDataSourceTest.java | 10 ++++++---- .../android/exoplayer2/upstream/ContentDataSource.java | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java index 10a408c578..d8743a0a2c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -33,6 +33,7 @@ import java.io.IOException; */ public final class ContentDataSourceTest extends InstrumentationTestCase { + private static final String AUTHORITY = "com.google.android.exoplayer2.core.test"; private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; private static final long DATA_LENGTH = 1024; @@ -40,7 +41,7 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); Uri contentUri = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) - .authority(TestContentProvider.AUTHORITY) + .authority(AUTHORITY) .path(DATA_PATH).build(); DataSpec dataSpec = new DataSpec(contentUri); try { @@ -57,7 +58,7 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); Uri contentUri = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) - .authority(TestContentProvider.AUTHORITY) + .authority(AUTHORITY) .build(); DataSpec dataSpec = new DataSpec(contentUri); try { @@ -70,10 +71,11 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { } } + /** + * A {@link ContentProvider} for the test. + */ public static final class TestContentProvider extends ContentProvider { - private static final String AUTHORITY = "com.google.android.exoplayer2.core.test"; - @Override public boolean onCreate() { return true; 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 507162519a..d118b91378 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 @@ -91,9 +91,9 @@ public final class ContentDataSource implements DataSource { // The asset must extend to the end of the file. bytesRemaining = inputStream.available(); if (bytesRemaining == 0) { - // FileInputStream.available() returns 0 if the remaining length cannot be determined, or - // if it's greater than Integer.MAX_VALUE. We don't know the true length in either case, - // so treat as unbounded. + // FileInputStream.available() returns 0 if the remaining length cannot be determined, + // or if it's greater than Integer.MAX_VALUE. We don't know the true length in either + // case, so treat as unbounded. bytesRemaining = C.LENGTH_UNSET; } } From 0572d190fee3992170b2cd3a7cbced01ac8b3ebf Mon Sep 17 00:00:00 2001 From: Alex Birkett Date: Fri, 23 Jun 2017 18:56:40 +0200 Subject: [PATCH 160/220] Make OkHttpDataSource userAgent parameter optional --- .../ext/okhttp/OkHttpDataSource.java | 21 ++++++++++++------- .../ext/okhttp/OkHttpDataSourceFactory.java | 11 ++++++---- 2 files changed, 20 insertions(+), 12 deletions(-) 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 47850c0637..fac35bd427 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 @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.ext.okhttp; import android.net.Uri; +import android.support.annotation.Nullable; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; @@ -67,11 +69,11 @@ public class OkHttpDataSource implements HttpDataSource { /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. - * @param userAgent The User-Agent string that should be used. + * @param userAgent An optional User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}. */ - public OkHttpDataSource(Call.Factory callFactory, String userAgent, + public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent, Predicate contentTypePredicate) { this(callFactory, userAgent, contentTypePredicate, null); } @@ -79,13 +81,13 @@ public class OkHttpDataSource implements HttpDataSource { /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. - * @param userAgent The User-Agent string that should be used. + * @param userAgent An optional 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 InvalidContentTypeException} is thrown from * {@link #open(DataSpec)}. * @param listener An optional listener. */ - public OkHttpDataSource(Call.Factory callFactory, String userAgent, + public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent, Predicate contentTypePredicate, TransferListener listener) { this(callFactory, userAgent, contentTypePredicate, listener, null, null); } @@ -93,7 +95,7 @@ public class OkHttpDataSource implements HttpDataSource { /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. - * @param userAgent The User-Agent string that should be used. + * @param userAgent An optional 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 InvalidContentTypeException} is thrown from * {@link #open(DataSpec)}. @@ -102,11 +104,11 @@ public class OkHttpDataSource implements HttpDataSource { * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to * the server as HTTP headers on every request. */ - public OkHttpDataSource(Call.Factory callFactory, String userAgent, + public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent, Predicate contentTypePredicate, TransferListener listener, CacheControl cacheControl, RequestProperties defaultRequestProperties) { this.callFactory = Assertions.checkNotNull(callFactory); - this.userAgent = Assertions.checkNotEmpty(userAgent); + this.userAgent = userAgent; this.contentTypePredicate = contentTypePredicate; this.listener = listener; this.cacheControl = cacheControl; @@ -280,7 +282,10 @@ public class OkHttpDataSource implements HttpDataSource { } builder.addHeader("Range", rangeRequest); } - builder.addHeader("User-Agent", userAgent); + if (userAgent != null) { + builder.addHeader("User-Agent", userAgent); + } + if (!allowGzip) { builder.addHeader("Accept-Encoding", "identity"); } 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 5228065db1..6ee09df7de 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 @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.ext.okhttp; +import android.support.annotation.Nullable; + import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; @@ -36,10 +38,10 @@ public final class OkHttpDataSourceFactory extends BaseFactory { /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the sources created by the factory. - * @param userAgent The User-Agent string that should be used. + * @param userAgent An optional User-Agent string that should be used. * @param listener An optional listener. */ - public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent, + public OkHttpDataSourceFactory(Call.Factory callFactory, @Nullable String userAgent, TransferListener listener) { this(callFactory, userAgent, listener, null); } @@ -47,11 +49,12 @@ public final class OkHttpDataSourceFactory extends BaseFactory { /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the sources created by the factory. - * @param userAgent The User-Agent string that should be used. + * @param userAgent An optional User-Agent string that should be used. * @param listener An optional listener. * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. */ - public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent, + public OkHttpDataSourceFactory(Call.Factory callFactory, + @Nullable String userAgent, TransferListener listener, CacheControl cacheControl) { this.callFactory = callFactory; this.userAgent = userAgent; From 363f2414d141b31c311630fa84dff1edb20c0a30 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 23 Jun 2017 07:57:19 -0700 Subject: [PATCH 161/220] Fix check for last period index in ExoPlayerImplInternal The if clause was never executed because nextLoadingPeriodIndex is set to C.INDEX_UNSET instead of loadingPeriodIndex + 1. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159948661 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 38b9648162..65dea43d08 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 @@ -1315,7 +1315,7 @@ import java.io.IOException; repeatMode); } - if (newLoadingPeriodIndex >= timeline.getPeriodCount()) { + if (newLoadingPeriodIndex == C.INDEX_UNSET) { // The next period is not available yet. mediaSource.maybeThrowSourceInfoRefreshError(); return; From c007e93ab0473a03c9dfe3202de7c51e3af143df Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 23 Jun 2017 09:44:27 -0700 Subject: [PATCH 162/220] Fix setSelectionOverride(index, tracks, null) Issue: #2988 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159958591 --- .../MappingTrackSelectorTest.java | 196 ++++++++++++++++++ .../android/exoplayer2/source/TrackGroup.java | 2 +- .../trackselection/MappingTrackSelector.java | 8 +- 3 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java new file mode 100644 index 0000000000..c31c651384 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java @@ -0,0 +1,196 @@ +/* + * 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.trackselection; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.util.MimeTypes; +import junit.framework.TestCase; + +/** + * Unit tests for {@link MappingTrackSelector}. + */ +public final class MappingTrackSelectorTest extends TestCase { + + private static final RendererCapabilities VIDEO_CAPABILITIES = + new FakeRendererCapabilities(C.TRACK_TYPE_VIDEO); + private static final RendererCapabilities AUDIO_CAPABILITIES = + new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO); + private static final RendererCapabilities[] RENDERER_CAPABILITIES = new RendererCapabilities[] { + VIDEO_CAPABILITIES, AUDIO_CAPABILITIES + }; + + private static final TrackGroup VIDEO_TRACK_GROUP = new TrackGroup( + Format.createVideoSampleFormat("video", MimeTypes.VIDEO_H264, null, Format.NO_VALUE, + Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, null)); + private static final TrackGroup AUDIO_TRACK_GROUP = new TrackGroup( + Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, 0, null)); + private static final TrackGroupArray TRACK_GROUPS = new TrackGroupArray( + VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP); + + private static final TrackSelection[] TRACK_SELECTIONS = new TrackSelection[] { + new FixedTrackSelection(VIDEO_TRACK_GROUP, 0), + new FixedTrackSelection(AUDIO_TRACK_GROUP, 0) + }; + + /** + * Tests that the video and audio track groups are mapped onto the correct renderers. + */ + public void testMapping() throws ExoPlaybackException { + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(); + trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); + trackSelector.assertMappedTrackGroups(0, VIDEO_TRACK_GROUP); + trackSelector.assertMappedTrackGroups(1, AUDIO_TRACK_GROUP); + } + + /** + * Tests that the video and audio track groups are mapped onto the correct renderers when the + * renderer ordering is reversed. + */ + public void testMappingReverseOrder() throws ExoPlaybackException { + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(); + RendererCapabilities[] reverseOrderRendererCapabilities = new RendererCapabilities[] { + AUDIO_CAPABILITIES, VIDEO_CAPABILITIES}; + trackSelector.selectTracks(reverseOrderRendererCapabilities, TRACK_GROUPS); + trackSelector.assertMappedTrackGroups(0, AUDIO_TRACK_GROUP); + trackSelector.assertMappedTrackGroups(1, VIDEO_TRACK_GROUP); + } + + /** + * Tests video and audio track groups are mapped onto the correct renderers when there are + * multiple track groups of the same type. + */ + public void testMappingMulti() throws ExoPlaybackException { + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(); + TrackGroupArray multiTrackGroups = new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, + VIDEO_TRACK_GROUP); + trackSelector.selectTracks(RENDERER_CAPABILITIES, multiTrackGroups); + trackSelector.assertMappedTrackGroups(0, VIDEO_TRACK_GROUP, VIDEO_TRACK_GROUP); + trackSelector.assertMappedTrackGroups(1, AUDIO_TRACK_GROUP); + } + + /** + * Tests the result of {@link MappingTrackSelector#selectTracks(RendererCapabilities[], + * TrackGroupArray[], int[][][])} is propagated correctly to the result of + * {@link MappingTrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray)}. + */ + public void testSelectTracks() throws ExoPlaybackException { + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); + TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); + assertEquals(TRACK_SELECTIONS[0], result.selections.get(0)); + assertEquals(TRACK_SELECTIONS[1], result.selections.get(1)); + } + + /** + * Tests that a null override clears a track selection. + */ + public void testSelectTracksWithNullOverride() throws ExoPlaybackException { + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); + trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); + TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); + assertNull(result.selections.get(0)); + assertEquals(TRACK_SELECTIONS[1], result.selections.get(1)); + } + + /** + * Tests that a null override can be cleared. + */ + public void testSelectTracksWithClearedNullOverride() throws ExoPlaybackException { + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); + trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); + trackSelector.clearSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP)); + TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); + assertEquals(TRACK_SELECTIONS[0], result.selections.get(0)); + assertEquals(TRACK_SELECTIONS[1], result.selections.get(1)); + } + + /** + * Tests that an override is not applied for a different set of available track groups. + */ + public void testSelectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException { + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); + trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); + TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, + new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, VIDEO_TRACK_GROUP)); + assertEquals(TRACK_SELECTIONS[0], result.selections.get(0)); + assertEquals(TRACK_SELECTIONS[1], result.selections.get(1)); + } + + /** + * A {@link MappingTrackSelector} that returns a fixed result from + * {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])}. + */ + private static final class FakeMappingTrackSelector extends MappingTrackSelector { + + private final TrackSelection[] result; + private TrackGroupArray[] lastRendererTrackGroupArrays; + + public FakeMappingTrackSelector(TrackSelection... result) { + this.result = result.length == 0 ? null : result; + } + + @Override + protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, + TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) + throws ExoPlaybackException { + lastRendererTrackGroupArrays = rendererTrackGroupArrays; + return result == null ? new TrackSelection[rendererCapabilities.length] : result; + } + + public void assertMappedTrackGroups(int rendererIndex, TrackGroup... expected) { + assertEquals(expected.length, lastRendererTrackGroupArrays[rendererIndex].length); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], lastRendererTrackGroupArrays[rendererIndex].get(i)); + } + } + + } + + /** + * A {@link RendererCapabilities} that advertises adaptive support for all tracks of a given type. + */ + private static final class FakeRendererCapabilities implements RendererCapabilities { + + private final int trackType; + + public FakeRendererCapabilities(int trackType) { + this.trackType = trackType; + } + + @Override + public int getTrackType() { + return trackType; + } + + @Override + public int supportsFormat(Format format) throws ExoPlaybackException { + return MimeTypes.getTrackType(format.sampleMimeType) == trackType + ? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE; + } + + @Override + public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { + return ADAPTIVE_SEAMLESS; + } + + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java index 393ac1988a..06410d5426 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java @@ -42,7 +42,7 @@ public final class TrackGroup { private int hashCode; /** - * @param formats The track formats. Must not be null or contain null elements. + * @param formats The track formats. Must not be null, contain null elements or be of length 0. */ public TrackGroup(Format... formats) { Assertions.checkState(formats.length > 0); 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 690723cf15..3499efdb16 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 @@ -304,10 +304,10 @@ public abstract class MappingTrackSelector extends TrackSelector { trackSelections[i] = null; } else { TrackGroupArray rendererTrackGroup = rendererTrackGroupArrays[i]; - Map overrides = selectionOverrides.get(i); - SelectionOverride override = overrides == null ? null : overrides.get(rendererTrackGroup); - if (override != null) { - trackSelections[i] = override.createTrackSelection(rendererTrackGroup); + if (hasSelectionOverride(i, rendererTrackGroup)) { + SelectionOverride override = selectionOverrides.get(i).get(rendererTrackGroup); + trackSelections[i] = override == null ? null + : override.createTrackSelection(rendererTrackGroup); } } } From b0a873df258c1f1f81f37622ec03746c6ec509b0 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 25 Jun 2017 15:16:11 +0100 Subject: [PATCH 163/220] Clean up okhttp datasource. --- .../ext/okhttp/OkHttpDataSource.java | 39 ++++++++++--------- .../ext/okhttp/OkHttpDataSourceFactory.java | 24 ++++++------ 2 files changed, 33 insertions(+), 30 deletions(-) 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 fac35bd427..167fc68e86 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 @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.ext.okhttp; import android.net.Uri; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; - import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; @@ -47,13 +47,14 @@ public class OkHttpDataSource implements HttpDataSource { private static final AtomicReference skipBufferReference = new AtomicReference<>(); - private final Call.Factory callFactory; - private final String userAgent; - private final Predicate contentTypePredicate; - private final TransferListener listener; - private final CacheControl cacheControl; - private final RequestProperties defaultRequestProperties; - private final RequestProperties requestProperties; + @NonNull private final Call.Factory callFactory; + @NonNull private final RequestProperties requestProperties; + + @Nullable private final String userAgent; + @Nullable private final Predicate contentTypePredicate; + @Nullable private final TransferListener listener; + @Nullable private final CacheControl cacheControl; + @Nullable private final RequestProperties defaultRequestProperties; private DataSpec dataSpec; private Response response; @@ -69,33 +70,34 @@ public class OkHttpDataSource implements HttpDataSource { /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. - * @param userAgent An optional User-Agent string that should be used. + * @param userAgent An optional User-Agent string. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}. */ - public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent, - Predicate contentTypePredicate) { + public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent, + @Nullable Predicate contentTypePredicate) { this(callFactory, userAgent, contentTypePredicate, null); } /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. - * @param userAgent An optional User-Agent string that should be used. + * @param userAgent An optional User-Agent string. * @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)}. * @param listener An optional listener. */ - public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent, - Predicate contentTypePredicate, TransferListener listener) { + public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent, + @Nullable Predicate contentTypePredicate, + @Nullable TransferListener listener) { this(callFactory, userAgent, contentTypePredicate, listener, null, null); } /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. - * @param userAgent An optional User-Agent string that should be used. + * @param userAgent An optional User-Agent string. * @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)}. @@ -104,9 +106,10 @@ public class OkHttpDataSource implements HttpDataSource { * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to * the server as HTTP headers on every request. */ - public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent, - Predicate contentTypePredicate, TransferListener listener, - CacheControl cacheControl, RequestProperties defaultRequestProperties) { + public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent, + @Nullable Predicate contentTypePredicate, + @Nullable TransferListener listener, + @Nullable CacheControl cacheControl, @Nullable RequestProperties defaultRequestProperties) { this.callFactory = Assertions.checkNotNull(callFactory); this.userAgent = userAgent; this.contentTypePredicate = contentTypePredicate; 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 6ee09df7de..32fc5a58cb 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 @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.ext.okhttp; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; - import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; @@ -30,32 +30,32 @@ import okhttp3.Call; */ public final class OkHttpDataSourceFactory extends BaseFactory { - private final Call.Factory callFactory; - private final String userAgent; - private final TransferListener listener; - private final CacheControl cacheControl; + @NonNull private final Call.Factory callFactory; + @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 * by the sources created by the factory. - * @param userAgent An optional User-Agent string that should be used. + * @param userAgent An optional User-Agent string. * @param listener An optional listener. */ - public OkHttpDataSourceFactory(Call.Factory callFactory, @Nullable String userAgent, - TransferListener listener) { + public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent, + @Nullable TransferListener listener) { this(callFactory, userAgent, listener, null); } /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the sources created by the factory. - * @param userAgent An optional User-Agent string that should be used. + * @param userAgent An optional User-Agent string. * @param listener An optional listener. * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. */ - public OkHttpDataSourceFactory(Call.Factory callFactory, - @Nullable String userAgent, - TransferListener listener, CacheControl cacheControl) { + public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent, + @Nullable TransferListener listener, + @Nullable CacheControl cacheControl) { this.callFactory = callFactory; this.userAgent = userAgent; this.listener = listener; From 70e6dd5930f912f261e0a39ece3564e747de558f Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 26 Jun 2017 02:46:42 -0700 Subject: [PATCH 164/220] Update DrmSessionException. Make DrmSessionException takes in Throwable cause instead of Exception cause, which is more limiting and doesn't add any benefit. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160121486 --- .../java/com/google/android/exoplayer2/drm/DrmSession.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 538db9e1d9..cd694396b7 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,11 +28,11 @@ import java.util.Map; @TargetApi(16) public interface DrmSession { - /** Wraps the exception which is the cause of the error state. */ + /** Wraps the throwable which is the cause of the error state. */ class DrmSessionException extends Exception { - public DrmSessionException(Exception e) { - super(e); + public DrmSessionException(Throwable cause) { + super(cause); } } From b3c6f6fb31c95cca4ff10a93318680c66cd1da12 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 26 Jun 2017 04:02:03 -0700 Subject: [PATCH 165/220] Merge ContentDataSource fixes + tests from GitHub https://github.com/google/ExoPlayer/pull/2963/files https://github.com/google/ExoPlayer/commit/8bb643976fe20d1ec684291aa7bf5337e474bec4 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160128047 --- .../upstream/AssetDataSourceTest.java | 35 +++++-------------- .../upstream/ContentDataSourceTest.java | 13 ++----- .../android/exoplayer2/testutil/TestUtil.java | 22 ++++++++++++ 3 files changed, 33 insertions(+), 37 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java index d8e61eb94c..102c89ec2b 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java @@ -15,10 +15,8 @@ */ package com.google.android.exoplayer2.upstream; -import android.content.Context; import android.net.Uri; import android.test.InstrumentationTestCase; -import android.test.MoreAsserts; import com.google.android.exoplayer2.testutil.TestUtil; /** @@ -27,36 +25,19 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class AssetDataSourceTest extends InstrumentationTestCase { private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; - private static final long DATA_LENGTH = 1024; public void testReadFileUri() throws Exception { - Context context = getInstrumentation().getContext(); - AssetDataSource dataSource = new AssetDataSource(context); - Uri assetUri = Uri.parse("file:///android_asset/" + DATA_PATH); - DataSpec dataSpec = new DataSpec(assetUri); - try { - long length = dataSource.open(dataSpec); - assertEquals(DATA_LENGTH, length); - byte[] readData = TestUtil.readToEnd(dataSource); - MoreAsserts.assertEquals(TestUtil.getByteArray(getInstrumentation(), DATA_PATH), readData); - } finally { - dataSource.close(); - } + AssetDataSource dataSource = new AssetDataSource(getInstrumentation().getContext()); + DataSpec dataSpec = new DataSpec(Uri.parse("file:///android_asset/" + DATA_PATH)); + TestUtil.assertDataSourceContent(dataSource, dataSpec, + TestUtil.getByteArray(getInstrumentation(), DATA_PATH)); } public void testReadAssetUri() throws Exception { - Context context = getInstrumentation().getContext(); - AssetDataSource dataSource = new AssetDataSource(context); - Uri assetUri = Uri.parse("asset:///" + DATA_PATH); - DataSpec dataSpec = new DataSpec(assetUri); - try { - long length = dataSource.open(dataSpec); - assertEquals(DATA_LENGTH, length); - byte[] readData = TestUtil.readToEnd(dataSource); - MoreAsserts.assertEquals(TestUtil.getByteArray(getInstrumentation(), DATA_PATH), readData); - } finally { - dataSource.close(); - } + AssetDataSource dataSource = new AssetDataSource(getInstrumentation().getContext()); + DataSpec dataSpec = new DataSpec(Uri.parse("asset:///" + DATA_PATH)); + TestUtil.assertDataSourceContent(dataSource, dataSpec, + TestUtil.getByteArray(getInstrumentation(), DATA_PATH)); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java index d8743a0a2c..834e7e1374 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -23,7 +23,6 @@ import android.database.Cursor; import android.net.Uri; import android.support.annotation.NonNull; import android.test.InstrumentationTestCase; -import android.test.MoreAsserts; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.FileNotFoundException; import java.io.IOException; @@ -35,7 +34,6 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { private static final String AUTHORITY = "com.google.android.exoplayer2.core.test"; private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; - private static final long DATA_LENGTH = 1024; public void testReadValidUri() throws Exception { ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); @@ -44,14 +42,8 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { .authority(AUTHORITY) .path(DATA_PATH).build(); DataSpec dataSpec = new DataSpec(contentUri); - try { - long length = dataSource.open(dataSpec); - assertEquals(DATA_LENGTH, length); - byte[] readData = TestUtil.readToEnd(dataSource); - MoreAsserts.assertEquals(TestUtil.getByteArray(getInstrumentation(), DATA_PATH), readData); - } finally { - dataSource.close(); - } + TestUtil.assertDataSourceContent(dataSource, dataSpec, + TestUtil.getByteArray(getInstrumentation(), DATA_PATH)); } public void testReadInvalidUri() throws Exception { @@ -66,6 +58,7 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { fail(); } catch (ContentDataSource.ContentDataSourceException e) { // Expected. + assertTrue(e.getCause() instanceof FileNotFoundException); } finally { dataSource.close(); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index f75239318a..363f60b10d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -17,12 +17,14 @@ package com.google.android.exoplayer2.testutil; import android.app.Instrumentation; import android.test.InstrumentationTestCase; +import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -390,4 +392,24 @@ public class TestUtil { } } + /** + * Asserts that data read from a {@link DataSource} matches {@code expected}. + * + * @param dataSource The {@link DataSource} through which to read. + * @param dataSpec The {@link DataSpec} to use when opening the {@link DataSource}. + * @param expectedData The expected data. + * @throws IOException If an error occurs reading fom the {@link DataSource}. + */ + public static void assertDataSourceContent(DataSource dataSource, DataSpec dataSpec, + byte[] expectedData) throws IOException { + try { + long length = dataSource.open(dataSpec); + Assert.assertEquals(length, expectedData.length); + byte[] readData = TestUtil.readToEnd(dataSource); + MoreAsserts.assertEquals(expectedData, readData); + } finally { + dataSource.close(); + } + } + } From 1b71e3b40ddec3c61bef721dd27f8bb27f3130d1 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 26 Jun 2017 04:09:59 -0700 Subject: [PATCH 166/220] Move ExtractorMediaPeriod to new SampleQueue methods This change allows you to enable/disable tracks within which all samples are key-frames without any re-buffering (e.g. audio, text and metadata). This effectively reverts V2 back to the behavior in V1, only this time we're doing it properly. []ly disabling/enabling, or disabling/enabling whilst paused, no longer cause samples to get "lost" between the source and renderers. Note it also becomes really easy to support a few other things, although support is not exposed in this change: - Enable/disable video tracks without any re-buffering, by changing the toKeyframe argument passed to discardTo to true. - Retain media in the buffer for some time after it's been played (e.g. to support a single back-5s-seek efficiently), by subtracting the desired back-buffer time from the value that's passed to discardTo. Issue: #2956 Issue: #2926 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160128586 --- .../source/ExtractorMediaPeriod.java | 94 +++++++++++-------- .../android/exoplayer2/upstream/Loader.java | 70 +++++++++++--- 2 files changed, 109 insertions(+), 55 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 62b1e85456..10d20c4800 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -47,7 +47,8 @@ import java.io.IOException; * A {@link MediaPeriod} that extracts data using an {@link Extractor}. */ /* package */ final class ExtractorMediaPeriod implements MediaPeriod, ExtractorOutput, - Loader.Callback, UpstreamFormatChangedListener { + Loader.Callback, Loader.ReleaseCallback, + UpstreamFormatChangedListener { /** * When the source's duration is unknown, it is calculated by adding this value to the largest @@ -146,21 +147,27 @@ import java.io.IOException; } public void release() { - final ExtractorHolder extractorHolder = this.extractorHolder; - loader.release(new Runnable() { - @Override - public void run() { - extractorHolder.release(); - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - sampleQueues.valueAt(i).disable(); - } + boolean releasedSynchronously = loader.release(this); + if (!releasedSynchronously) { + // Discard as much as we can synchronously. + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { + sampleQueues.valueAt(i).discardToEnd(); } - }); + } handler.removeCallbacksAndMessages(null); released = true; } + @Override + public void onLoaderReleased() { + extractorHolder.release(); + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { + sampleQueues.valueAt(i).reset(true); + } + } + @Override public void prepare(Callback callback, long positionUs) { this.callback = callback; @@ -182,19 +189,21 @@ import java.io.IOException; public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { Assertions.checkState(prepared); - // Disable old tracks. + int oldEnabledTrackCount = enabledTrackCount; + // Deselect old tracks. for (int i = 0; i < selections.length; i++) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { int track = ((SampleStreamImpl) streams[i]).track; Assertions.checkState(trackEnabledStates[track]); enabledTrackCount--; trackEnabledStates[track] = false; - sampleQueues.valueAt(track).disable(); streams[i] = null; } } - // Enable new tracks. - boolean selectedNewTracks = false; + // We'll always need to seek if this is a first selection to a non-zero position, or if we're + // making a selection having previously disabled all tracks. + boolean seekRequired = seenFirstTrackSelection ? oldEnabledTrackCount == 0 : positionUs != 0; + // Select new tracks. for (int i = 0; i < selections.length; i++) { if (streams[i] == null && selections[i] != null) { TrackSelection selection = selections[i]; @@ -206,16 +215,12 @@ import java.io.IOException; trackEnabledStates[track] = true; streams[i] = new SampleStreamImpl(track); streamResetFlags[i] = true; - selectedNewTracks = true; - } - } - if (!seenFirstTrackSelection) { - // At the time of the first track selection all queues will be enabled, so we need to disable - // any that are no longer required. - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - if (!trackEnabledStates[i]) { - sampleQueues.valueAt(i).disable(); + // If there's still a chance of avoiding a seek, try and seek within the sample queue. + if (!seekRequired) { + SampleQueue sampleQueue = sampleQueues.valueAt(i); + sampleQueue.rewind(); + seekRequired = !sampleQueue.advanceTo(positionUs, true, true) + && sampleQueue.getReadIndex() != 0; } } } @@ -224,7 +229,7 @@ import java.io.IOException; if (loader.isLoading()) { loader.cancelLoading(); } - } else if (seenFirstTrackSelection ? selectedNewTracks : positionUs != 0) { + } else if (seekRequired) { positionUs = seekToUs(positionUs); // We'll need to reset renderers consuming from all streams due to the seek. for (int i = 0; i < streams.length; i++) { @@ -239,7 +244,10 @@ import java.io.IOException; @Override public void discardBuffer(long positionUs) { - // Do nothing. + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { + sampleQueues.valueAt(i).discardTo(positionUs, false, trackEnabledStates[i]); + } } @Override @@ -303,9 +311,13 @@ import java.io.IOException; // If we're not pending a reset, see if we can seek within the sample queues. boolean seekInsideBuffer = !isPendingReset(); for (int i = 0; seekInsideBuffer && i < trackCount; i++) { - if (trackEnabledStates[i]) { - seekInsideBuffer = sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs, false); - } + SampleQueue sampleQueue = sampleQueues.valueAt(i); + sampleQueue.rewind(); + // TODO: For sparse tracks (e.g. text, metadata) this may return false when an in-buffer + // seek should be allowed. If there are non-sparse tracks (e.g. video, audio) for which + // in-buffer seeking is successful, we should perform an in-buffer seek unconditionally. + seekInsideBuffer = sampleQueue.advanceTo(positionUs, true, false); + sampleQueue.discardToRead(); } // If we failed to seek within the sample queues, we need to restart. if (!seekInsideBuffer) { @@ -338,17 +350,16 @@ import java.io.IOException; if (notifyReset || isPendingReset()) { return C.RESULT_NOTHING_READ; } - - return sampleQueues.valueAt(track).readData(formatHolder, buffer, formatRequired, + return sampleQueues.valueAt(track).read(formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); } /* package */ void skipData(int track, long positionUs) { SampleQueue sampleQueue = sampleQueues.valueAt(track); if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - sampleQueue.skipAll(); + sampleQueue.advanceToEnd(); } else { - sampleQueue.skipToKeyframeBefore(positionUs, true); + sampleQueue.advanceTo(positionUs, true, true); } } @@ -372,12 +383,15 @@ import java.io.IOException; @Override public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { + if (released) { + return; + } copyLengthFromLoader(loadable); - if (!released && enabledTrackCount > 0) { - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - sampleQueues.valueAt(i).reset(trackEnabledStates[i]); - } + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { + sampleQueues.valueAt(i).reset(true); + } + if (enabledTrackCount > 0) { callback.onContinueLoadingRequested(this); } } @@ -508,7 +522,7 @@ import java.io.IOException; notifyReset = prepared; int trackCount = sampleQueues.size(); for (int i = 0; i < trackCount; i++) { - sampleQueues.valueAt(i).reset(!prepared || trackEnabledStates[i]); + sampleQueues.valueAt(i).reset(true); } loadable.setLoadPosition(0, 0); } 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 1bdebf7c17..02ccfafa89 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 @@ -119,17 +119,23 @@ public final class Loader implements LoaderErrorThrower { } + /** + * A callback to be notified when a {@link Loader} has finished being released. + */ + public interface ReleaseCallback { + + /** + * Called when the {@link Loader} has finished being released. + */ + void onLoaderReleased(); + + } + public static final int RETRY = 0; public static final int RETRY_RESET_ERROR_COUNT = 1; public static final int DONT_RETRY = 2; public static final int DONT_RETRY_FATAL = 3; - private static final int MSG_START = 0; - private static final int MSG_CANCEL = 1; - private static final int MSG_END_OF_SOURCE = 2; - private static final int MSG_IO_EXCEPTION = 3; - private static final int MSG_FATAL_ERROR = 4; - private final ExecutorService downloadExecutorService; private LoadTask currentTask; @@ -150,7 +156,7 @@ public final class Loader implements LoaderErrorThrower { * * @param The type of the loadable. * @param loadable The {@link Loadable} to load. - * @param callback A callback to called when the load ends. + * @param callback A callback to be called when the load ends. * @param defaultMinRetryCount The minimum number of times the load must be retried before * {@link #maybeThrowError()} will propagate an error. * @throws IllegalStateException If the calling thread does not have an associated {@link Looper}. @@ -188,20 +194,28 @@ public final class Loader implements LoaderErrorThrower { } /** - * Releases the {@link Loader}, running {@code postLoadAction} on its thread. This method should - * be called when the {@link Loader} is no longer required. + * Releases the {@link Loader}. This method should be called when the {@link Loader} is no longer + * required. * - * @param postLoadAction A {@link Runnable} to run on the loader's thread when - * {@link Loadable#load()} is no longer running. + * @param callback A callback to be called when the release ends. Will be called synchronously + * from this method if no load is in progress, or asynchronously once the load has been + * canceled otherwise. May be null. + * @return True if {@code callback} was called synchronously. False if it will be called + * asynchronously or if {@code callback} is null. */ - public void release(Runnable postLoadAction) { + public boolean release(ReleaseCallback callback) { + boolean callbackInvoked = false; if (currentTask != null) { currentTask.cancel(true); - } - if (postLoadAction != null) { - downloadExecutorService.execute(postLoadAction); + if (callback != null) { + downloadExecutorService.execute(new ReleaseTask(callback)); + } + } else if (callback != null) { + callback.onLoaderReleased(); + callbackInvoked = true; } downloadExecutorService.shutdown(); + return callbackInvoked; } // LoaderErrorThrower implementation. @@ -228,6 +242,12 @@ public final class Loader implements LoaderErrorThrower { private static final String TAG = "LoadTask"; + private static final int MSG_START = 0; + private static final int MSG_CANCEL = 1; + private static final int MSG_END_OF_SOURCE = 2; + private static final int MSG_IO_EXCEPTION = 3; + private static final int MSG_FATAL_ERROR = 4; + private final T loadable; private final Loader.Callback callback; public final int defaultMinRetryCount; @@ -390,4 +410,24 @@ public final class Loader implements LoaderErrorThrower { } + private static final class ReleaseTask extends Handler implements Runnable { + + private final ReleaseCallback callback; + + public ReleaseTask(ReleaseCallback callback) { + this.callback = callback; + } + + @Override + public void run() { + sendEmptyMessage(0); + } + + @Override + public void handleMessage(Message msg) { + callback.onLoaderReleased(); + } + + } + } From c448463a05e31046d5dc4695aeebae58086a0393 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 26 Jun 2017 04:45:01 -0700 Subject: [PATCH 167/220] Move DashMediaSource and SsMediaSource to new SampleQueue methods ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160130540 --- .../source/chunk/ChunkSampleStream.java | 87 +++++++++++-------- .../source/dash/DashMediaPeriod.java | 2 +- 2 files changed, 52 insertions(+), 37 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 d7c9174a89..f4bdbc1676 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 @@ -36,7 +36,7 @@ import java.util.List; * May also be configured to expose additional embedded {@link SampleStream}s. */ public class ChunkSampleStream implements SampleStream, SequenceableLoader, - Loader.Callback { + Loader.Callback, Loader.ReleaseCallback { private final int primaryTrackType; private final int[] embeddedTrackTypes; @@ -106,17 +106,20 @@ public class ChunkSampleStream implements SampleStream, S lastSeekPositionUs = positionUs; } + // TODO: Generalize this method to also discard from the primary sample queue and stop discarding + // from this queue in readData and skipData. This will cause samples to be kept in the queue until + // they've been rendered, rather than being discarded as soon as they're read by the renderer. + // This will make in-buffer seeks more likely when seeking slightly forward from the current + // position. This change will need handling with care, in particular when considering removal of + // chunks from the front of the mediaChunks list. /** - * Discards buffered media for embedded tracks that are not currently selected, up to the - * specified position. + * Discards buffered media for embedded tracks, up to the specified position. * * @param positionUs The position to discard up to, in microseconds. */ - public void discardUnselectedEmbeddedTracksTo(long positionUs) { + public void discardEmbeddedTracksTo(long positionUs) { for (int i = 0; i < embeddedSampleQueues.length; i++) { - if (!embeddedTracksSelected[i]) { - embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true); - } + embeddedSampleQueues[i].discardTo(positionUs, true, embeddedTracksSelected[i]); } } @@ -135,7 +138,8 @@ public class ChunkSampleStream implements SampleStream, S if (embeddedTrackTypes[i] == trackType) { Assertions.checkState(!embeddedTracksSelected[i]); embeddedTracksSelected[i] = true; - embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true); + embeddedSampleQueues[i].rewind(); + embeddedSampleQueues[i].advanceTo(positionUs, true, true); return new EmbeddedSampleStream(this, embeddedSampleQueues[i], i); } } @@ -181,19 +185,15 @@ public class ChunkSampleStream implements SampleStream, S public void seekToUs(long positionUs) { lastSeekPositionUs = positionUs; // If we're not pending a reset, see if we can seek within the primary sample queue. - boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.skipToKeyframeBefore( - positionUs, positionUs < getNextLoadPositionUs()); + boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.advanceTo(positionUs, true, + positionUs < getNextLoadPositionUs()); if (seekInsideBuffer) { - // We succeeded. We need to discard any chunks that we've moved past and perform the seek for - // any embedded streams as well. - while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex(0) <= primarySampleQueue.getReadIndex()) { - mediaChunks.removeFirst(); - } - // TODO: For this to work correctly, the embedded streams must not discard anything from their - // sample queues beyond the current read position of the primary stream. + // We succeeded. Discard samples and corresponding chunks prior to the seek position. + discardDownstreamMediaChunks(primarySampleQueue.getReadIndex()); + primarySampleQueue.discardToRead(); for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.skipToKeyframeBefore(positionUs, true); + embeddedSampleQueue.rewind(); + embeddedSampleQueue.discardTo(positionUs, true, false); } } else { // We failed, and need to restart. @@ -217,11 +217,22 @@ public class ChunkSampleStream implements SampleStream, S * This method should be called when the stream is no longer required. */ public void release() { - primarySampleQueue.disable(); - for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.disable(); + boolean releasedSynchronously = loader.release(this); + if (!releasedSynchronously) { + // Discard as much as we can synchronously. + primarySampleQueue.discardToEnd(); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.discardToEnd(); + } + } + } + + @Override + public void onLoaderReleased() { + primarySampleQueue.reset(true); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.reset(true); } - loader.release(); } // SampleStream implementation. @@ -246,17 +257,22 @@ public class ChunkSampleStream implements SampleStream, S return C.RESULT_NOTHING_READ; } discardDownstreamMediaChunks(primarySampleQueue.getReadIndex()); - return primarySampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, + int result = primarySampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); + if (result == C.RESULT_BUFFER_READ) { + primarySampleQueue.discardToRead(); + } + return result; } @Override public void skipData(long positionUs) { if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { - primarySampleQueue.skipAll(); + primarySampleQueue.advanceToEnd(); } else { - primarySampleQueue.skipToKeyframeBefore(positionUs, true); + primarySampleQueue.advanceTo(positionUs, true, true); } + primarySampleQueue.discardToRead(); } // Loader.Callback implementation. @@ -416,18 +432,18 @@ public class ChunkSampleStream implements SampleStream, S if (mediaChunks.size() <= queueLength) { return false; } - long startTimeUs = 0; + BaseMediaChunk removed; + long startTimeUs; long endTimeUs = mediaChunks.getLast().endTimeUs; - BaseMediaChunk removed = null; - while (mediaChunks.size() > queueLength) { + do { removed = mediaChunks.removeLast(); startTimeUs = removed.startTimeUs; - loadingFinished = false; - } + } while (mediaChunks.size() > queueLength); primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); for (int i = 0; i < embeddedSampleQueues.length; i++) { embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); } + loadingFinished = false; eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs); return true; } @@ -442,8 +458,7 @@ public class ChunkSampleStream implements SampleStream, S private final SampleQueue sampleQueue; private final int index; - public EmbeddedSampleStream(ChunkSampleStream parent, SampleQueue sampleQueue, - int index) { + public EmbeddedSampleStream(ChunkSampleStream parent, SampleQueue sampleQueue, int index) { this.parent = parent; this.sampleQueue = sampleQueue; this.index = index; @@ -457,9 +472,9 @@ public class ChunkSampleStream implements SampleStream, S @Override public void skipData(long positionUs) { if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - sampleQueue.skipAll(); + sampleQueue.advanceToEnd(); } else { - sampleQueue.skipToKeyframeBefore(positionUs, true); + sampleQueue.advanceTo(positionUs, true, true); } } @@ -474,7 +489,7 @@ public class ChunkSampleStream implements SampleStream, S if (isPendingReset()) { return C.RESULT_NOTHING_READ; } - return sampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, + return sampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); } 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 905c82364a..d86cc8bae2 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 @@ -188,7 +188,7 @@ import java.util.List; @Override public void discardBuffer(long positionUs) { for (ChunkSampleStream sampleStream : sampleStreams) { - sampleStream.discardUnselectedEmbeddedTracksTo(positionUs); + sampleStream.discardEmbeddedTracksTo(positionUs); } } From acbddbc0a5e628946b6311a2d03ae82f530e7bd3 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 26 Jun 2017 06:50:08 -0700 Subject: [PATCH 168/220] Use regular array for SampleQueues in ExtractorMediaPeriod ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160138881 --- .../source/ExtractorMediaPeriod.java | 98 ++++++++++--------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 10d20c4800..8a35034bb7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; -import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; @@ -42,6 +41,7 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; +import java.util.Arrays; /** * A {@link MediaPeriod} that extracts data using an {@link Extractor}. @@ -71,11 +71,12 @@ import java.io.IOException; private final Runnable maybeFinishPrepareRunnable; private final Runnable onContinueLoadingRequestedRunnable; private final Handler handler; - private final SparseArray sampleQueues; private Callback callback; private SeekMap seekMap; - private boolean tracksBuilt; + private SampleQueue[] sampleQueues; + private int[] sampleQueueTrackIds; + private boolean sampleQueuesBuilt; private boolean prepared; private boolean seenFirstTrackSelection; @@ -140,19 +141,19 @@ import java.io.IOException; } }; handler = new Handler(); - + sampleQueueTrackIds = new int[0]; + sampleQueues = new SampleQueue[0]; pendingResetPositionUs = C.TIME_UNSET; - sampleQueues = new SparseArray<>(); length = C.LENGTH_UNSET; } public void release() { boolean releasedSynchronously = loader.release(this); - if (!releasedSynchronously) { - // Discard as much as we can synchronously. - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - sampleQueues.valueAt(i).discardToEnd(); + if (prepared && !releasedSynchronously) { + // Discard as much as we can synchronously. We only do this if we're prepared, since + // otherwise sampleQueues may still be being modified by the loading thread. + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.discardToEnd(); } } handler.removeCallbacksAndMessages(null); @@ -162,9 +163,8 @@ import java.io.IOException; @Override public void onLoaderReleased() { extractorHolder.release(); - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - sampleQueues.valueAt(i).reset(true); + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(true); } } @@ -217,7 +217,7 @@ import java.io.IOException; streamResetFlags[i] = true; // If there's still a chance of avoiding a seek, try and seek within the sample queue. if (!seekRequired) { - SampleQueue sampleQueue = sampleQueues.valueAt(i); + SampleQueue sampleQueue = sampleQueues[i]; sampleQueue.rewind(); seekRequired = !sampleQueue.advanceTo(positionUs, true, true) && sampleQueue.getReadIndex() != 0; @@ -244,9 +244,9 @@ import java.io.IOException; @Override public void discardBuffer(long positionUs) { - int trackCount = sampleQueues.size(); + int trackCount = sampleQueues.length; for (int i = 0; i < trackCount; i++) { - sampleQueues.valueAt(i).discardTo(positionUs, false, trackEnabledStates[i]); + sampleQueues[i].discardTo(positionUs, false, trackEnabledStates[i]); } } @@ -288,11 +288,11 @@ import java.io.IOException; if (haveAudioVideoTracks) { // Ignore non-AV tracks, which may be sparse or poorly interleaved. largestQueuedTimestampUs = Long.MAX_VALUE; - int trackCount = sampleQueues.size(); + int trackCount = sampleQueues.length; for (int i = 0; i < trackCount; i++) { if (trackIsAudioVideoFlags[i]) { largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs, - sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); + sampleQueues[i].getLargestQueuedTimestampUs()); } } } else { @@ -307,11 +307,11 @@ import java.io.IOException; // Treat all seeks into non-seekable media as being to t=0. positionUs = seekMap.isSeekable() ? positionUs : 0; lastSeekPositionUs = positionUs; - int trackCount = sampleQueues.size(); // If we're not pending a reset, see if we can seek within the sample queues. boolean seekInsideBuffer = !isPendingReset(); + int trackCount = sampleQueues.length; for (int i = 0; seekInsideBuffer && i < trackCount; i++) { - SampleQueue sampleQueue = sampleQueues.valueAt(i); + SampleQueue sampleQueue = sampleQueues[i]; sampleQueue.rewind(); // TODO: For sparse tracks (e.g. text, metadata) this may return false when an in-buffer // seek should be allowed. If there are non-sparse tracks (e.g. video, audio) for which @@ -327,7 +327,7 @@ import java.io.IOException; loader.cancelLoading(); } else { for (int i = 0; i < trackCount; i++) { - sampleQueues.valueAt(i).reset(trackEnabledStates[i]); + sampleQueues[i].reset(trackEnabledStates[i]); } } } @@ -338,7 +338,7 @@ import java.io.IOException; // SampleStream methods. /* package */ boolean isReady(int track) { - return loadingFinished || (!isPendingReset() && sampleQueues.valueAt(track).hasNextSample()); + return loadingFinished || (!isPendingReset() && sampleQueues[track].hasNextSample()); } /* package */ void maybeThrowError() throws IOException { @@ -350,12 +350,12 @@ import java.io.IOException; if (notifyReset || isPendingReset()) { return C.RESULT_NOTHING_READ; } - return sampleQueues.valueAt(track).read(formatHolder, buffer, formatRequired, - loadingFinished, lastSeekPositionUs); + return sampleQueues[track].read(formatHolder, buffer, formatRequired, loadingFinished, + lastSeekPositionUs); } /* package */ void skipData(int track, long positionUs) { - SampleQueue sampleQueue = sampleQueues.valueAt(track); + SampleQueue sampleQueue = sampleQueues[track]; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { sampleQueue.advanceToEnd(); } else { @@ -387,9 +387,8 @@ import java.io.IOException; return; } copyLengthFromLoader(loadable); - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - sampleQueues.valueAt(i).reset(true); + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(true); } if (enabledTrackCount > 0) { callback.onContinueLoadingRequested(this); @@ -415,18 +414,24 @@ import java.io.IOException; @Override public TrackOutput track(int id, int type) { - SampleQueue trackOutput = sampleQueues.get(id); - if (trackOutput == null) { - trackOutput = new SampleQueue(allocator); - trackOutput.setUpstreamFormatChangeListener(this); - sampleQueues.put(id, trackOutput); + int trackCount = sampleQueues.length; + for (int i = 0; i < trackCount; i++) { + if (sampleQueueTrackIds[i] == id) { + return sampleQueues[i]; + } } + SampleQueue trackOutput = new SampleQueue(allocator); + trackOutput.setUpstreamFormatChangeListener(this); + sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1); + sampleQueueTrackIds[trackCount] = id; + sampleQueues = Arrays.copyOf(sampleQueues, trackCount + 1); + sampleQueues[trackCount] = trackOutput; return trackOutput; } @Override public void endTracks() { - tracksBuilt = true; + sampleQueuesBuilt = true; handler.post(maybeFinishPrepareRunnable); } @@ -446,22 +451,22 @@ import java.io.IOException; // Internal methods. private void maybeFinishPrepare() { - if (released || prepared || seekMap == null || !tracksBuilt) { + if (released || prepared || seekMap == null || !sampleQueuesBuilt) { return; } - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - if (sampleQueues.valueAt(i).getUpstreamFormat() == null) { + for (SampleQueue sampleQueue : sampleQueues) { + if (sampleQueue.getUpstreamFormat() == null) { return; } } loadCondition.close(); + int trackCount = sampleQueues.length; TrackGroup[] trackArray = new TrackGroup[trackCount]; trackIsAudioVideoFlags = new boolean[trackCount]; trackEnabledStates = new boolean[trackCount]; durationUs = seekMap.getDurationUs(); for (int i = 0; i < trackCount; i++) { - Format trackFormat = sampleQueues.valueAt(i).getUpstreamFormat(); + Format trackFormat = sampleQueues[i].getUpstreamFormat(); trackArray[i] = new TrackGroup(trackFormat); String mimeType = trackFormat.sampleMimeType; boolean isAudioVideo = MimeTypes.isVideo(mimeType) || MimeTypes.isAudio(mimeType); @@ -520,9 +525,8 @@ import java.io.IOException; // a new load. lastSeekPositionUs = 0; notifyReset = prepared; - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - sampleQueues.valueAt(i).reset(true); + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(true); } loadable.setLoadPosition(0, 0); } @@ -530,19 +534,17 @@ import java.io.IOException; private int getExtractedSamplesCount() { int extractedSamplesCount = 0; - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - extractedSamplesCount += sampleQueues.valueAt(i).getWriteIndex(); + for (SampleQueue sampleQueue : sampleQueues) { + extractedSamplesCount += sampleQueue.getWriteIndex(); } return extractedSamplesCount; } private long getLargestQueuedTimestampUs() { long largestQueuedTimestampUs = Long.MIN_VALUE; - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { + for (SampleQueue sampleQueue : sampleQueues) { largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, - sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); + sampleQueue.getLargestQueuedTimestampUs()); } return largestQueuedTimestampUs; } From a7ed199622500584d5070335f2381288198a53e7 Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 26 Jun 2017 06:51:52 -0700 Subject: [PATCH 169/220] Fix FLAC extension native part compilation In the latest NDK version (r15) compilation fails because 'memset' isn't defined. Included cstring header. Issue: #2977 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160139022 --- extensions/flac/src/main/jni/flac_parser.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index e4925cb462..6c6e57f5f7 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -22,6 +22,7 @@ #include #include +#include #define LOG_TAG "FLACParser" #define ALOGE(...) \ From 3fbfe29d27ba84cdedafe9de09c4a36a004a6912 Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 26 Jun 2017 07:31:32 -0700 Subject: [PATCH 170/220] Modify CacheUtil.cache() for polling counters Getting active status of caching is needed to display on UI. Instead of a listener interface polling was chosen because of simplicity and better suits to UI refreshing. CachingCounters.downloadedBytes was updated after whole data is downloaded. Now it's updated for each read into buffer. Buffer length defines how finer these updates are. CachingCounters.totalBytes is added so UI can display a progress bar. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160142048 --- .../upstream/cache/CacheUtilTest.java | 297 ++++++++++++++++++ .../exoplayer2/upstream/cache/CacheUtil.java | 121 ++++--- 2 files changed, 381 insertions(+), 37 deletions(-) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java new file mode 100644 index 0000000000..110819d2dc --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java @@ -0,0 +1,297 @@ +/* + * 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.upstream.cache; + +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty; +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; + +import android.net.Uri; +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.FakeDataSource; +import com.google.android.exoplayer2.testutil.FakeDataSource.FakeDataSet; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; +import com.google.android.exoplayer2.util.Util; +import java.io.EOFException; +import java.io.File; +import org.mockito.Answers; +import org.mockito.Mock; + +/** + * Tests {@link CacheUtil}. + */ +public class CacheUtilTest extends InstrumentationTestCase { + + /** + * Abstract fake Cache implementation used by the test. This class must be public so Mockito can + * create a proxy for it. + */ + public abstract static class AbstractFakeCache implements Cache { + // This array is set to alternating length of cached and not cached regions in tests: + // spansAndGaps = {, , + // , , ... } + // Ideally it should end with a cached region but it shouldn't matter for any code. + private int[] spansAndGaps; + private long contentLength; + + private void init() { + spansAndGaps = new int[]{}; + contentLength = C.LENGTH_UNSET; + } + + @Override + public long getCachedBytes(String key, long position, long length) { + for (int i = 0; i < spansAndGaps.length; i++) { + int spanOrGap = spansAndGaps[i]; + if (position < spanOrGap) { + long left = Math.min(spanOrGap - position, length); + return (i & 1) == 1 ? -left : left; + } + position -= spanOrGap; + } + return -length; + } + + @Override + public long getContentLength(String key) { + return contentLength; + } + } + + @Mock(answer = Answers.CALLS_REAL_METHODS) private AbstractFakeCache mockCache; + private File tempFolder; + private SimpleCache cache; + + @Override + public void setUp() throws Exception { + super.setUp(); + TestUtil.setUpMockito(this); + mockCache.init(); + tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); + } + + @Override + public void tearDown() throws Exception { + Util.recursiveDelete(tempFolder); + super.tearDown(); + } + + public void testGenerateKey() throws Exception { + assertNotNull(CacheUtil.generateKey(Uri.EMPTY)); + + Uri testUri = Uri.parse("test"); + String key = CacheUtil.generateKey(testUri); + assertNotNull(key); + + // Should generate the same key for the same input + assertEquals(key, CacheUtil.generateKey(testUri)); + + // Should generate different key for different input + assertFalse(key.equals(CacheUtil.generateKey(Uri.parse("test2")))); + } + + public void testGetKey() throws Exception { + Uri testUri = Uri.parse("test"); + String key = "key"; + // If DataSpec.key is present, returns it + assertEquals(key, CacheUtil.getKey(new DataSpec(testUri, 0, C.LENGTH_UNSET, key))); + // If not generates a new one using DataSpec.uri + assertEquals(CacheUtil.generateKey(testUri), + CacheUtil.getKey(new DataSpec(testUri, 0, C.LENGTH_UNSET, null))); + } + + public void testGetCachedCachingCounters() throws Exception { + DataSpec dataSpec = new DataSpec(Uri.parse("test")); + CachingCounters counters = CacheUtil.getCached(dataSpec, mockCache, null); + // getCached should create a CachingCounters and return it + assertNotNull(counters); + + CachingCounters newCounters = CacheUtil.getCached(dataSpec, mockCache, counters); + // getCached should set and return given CachingCounters + assertEquals(counters, newCounters); + } + + public void testGetCachedNoData() throws Exception { + CachingCounters counters = + CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, null); + + assertCounters(counters, 0, 0, C.LENGTH_UNSET); + } + + public void testGetCachedDataUnknownLength() throws Exception { + // Mock there is 100 bytes cached at the beginning + mockCache.spansAndGaps = new int[]{100}; + CachingCounters counters = + CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, null); + + assertCounters(counters, 100, 0, C.LENGTH_UNSET); + } + + public void testGetCachedNoDataKnownLength() throws Exception { + mockCache.contentLength = 1000; + CachingCounters counters = + CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, null); + + assertCounters(counters, 0, 0, 1000); + } + + public void testGetCached() throws Exception { + mockCache.contentLength = 1000; + mockCache.spansAndGaps = new int[]{100, 100, 200}; + CachingCounters counters = + CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, null); + + assertCounters(counters, 300, 0, 1000); + } + + public void testCache() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + + CachingCounters counters = + CacheUtil.cache(new DataSpec(Uri.parse("test_data")), cache, dataSource, null); + + assertCounters(counters, 0, 100, 100); + assertCachedData(cache, fakeDataSet); + } + + public void testCacheSetOffsetAndLength() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + + Uri testUri = Uri.parse("test_data"); + DataSpec dataSpec = new DataSpec(testUri, 10, 20, null); + CachingCounters counters = CacheUtil.cache(dataSpec, cache, dataSource, null); + + assertCounters(counters, 0, 20, 20); + + CacheUtil.cache(new DataSpec(testUri), cache, dataSource, counters); + + assertCounters(counters, 20, 80, 100); + assertCachedData(cache, fakeDataSet); + } + + public void testCacheUnknownLength() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data") + .setSimulateUnknownLength(true) + .appendReadData(TestUtil.buildTestData(100)).endData(); + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + + DataSpec dataSpec = new DataSpec(Uri.parse("test_data")); + CachingCounters counters = CacheUtil.cache(dataSpec, cache, dataSource, null); + + assertCounters(counters, 0, 100, 100); + assertCachedData(cache, fakeDataSet); + } + + public void testCacheUnknownLengthPartialCaching() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data") + .setSimulateUnknownLength(true) + .appendReadData(TestUtil.buildTestData(100)).endData(); + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + + Uri testUri = Uri.parse("test_data"); + DataSpec dataSpec = new DataSpec(testUri, 10, 20, null); + CachingCounters counters = CacheUtil.cache(dataSpec, cache, dataSource, null); + + assertCounters(counters, 0, 20, 20); + + CacheUtil.cache(new DataSpec(testUri), cache, dataSource, counters); + + assertCounters(counters, 20, 80, 100); + assertCachedData(cache, fakeDataSet); + } + + public void testCacheLengthExceedsActualDataLength() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + + Uri testUri = Uri.parse("test_data"); + DataSpec dataSpec = new DataSpec(testUri, 0, 1000, null); + CachingCounters counters = CacheUtil.cache(dataSpec, cache, dataSource, null); + + assertCounters(counters, 0, 100, 1000); + assertCachedData(cache, fakeDataSet); + } + + public void testCacheThrowEOFException() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + + Uri testUri = Uri.parse("test_data"); + DataSpec dataSpec = new DataSpec(testUri, 0, 1000, null); + + try { + CacheUtil.cache(dataSpec, cache, new CacheDataSource(cache, dataSource), + new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], null, 0, null, + /*enableEOFException*/ true); + fail(); + } catch (EOFException e) { + // Do nothing. + } + } + + public void testCachePolling() throws Exception { + final CachingCounters counters = new CachingCounters(); + FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data") + .appendReadData(TestUtil.buildTestData(100)) + .appendReadAction(new Runnable() { + @Override + public void run() { + assertCounters(counters, 0, 100, 300); + } + }) + .appendReadData(TestUtil.buildTestData(100)) + .appendReadAction(new Runnable() { + @Override + public void run() { + assertCounters(counters, 0, 200, 300); + } + }) + .appendReadData(TestUtil.buildTestData(100)).endData(); + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + + CacheUtil.cache(new DataSpec(Uri.parse("test_data")), cache, dataSource, counters); + + assertCounters(counters, 0, 300, 300); + assertCachedData(cache, fakeDataSet); + } + + public void testRemove() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + + Uri uri = Uri.parse("test_data"); + CacheUtil.cache(new DataSpec(uri), cache, + // set maxCacheFileSize to 10 to make sure there are multiple spans + new CacheDataSource(cache, dataSource, 0, 10), + new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], null, 0, null, true); + CacheUtil.remove(cache, CacheUtil.generateKey(uri)); + + assertCacheEmpty(cache); + } + + private static void assertCounters(CachingCounters counters, int alreadyCachedBytes, + int downloadedBytes, int totalBytes) { + assertEquals(alreadyCachedBytes, counters.alreadyCachedBytes); + assertEquals(downloadedBytes, counters.downloadedBytes); + assertEquals(totalBytes, counters.totalBytes); + } + +} 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 bb1f88e5ea..22a7635564 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 @@ -22,28 +22,32 @@ import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.PriorityTaskManager; import com.google.android.exoplayer2.util.Util; +import java.io.EOFException; import java.io.IOException; import java.util.NavigableSet; /** * Caching related utility methods. */ +@SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"}) public final class CacheUtil { /** Holds the counters used during caching. */ public static class CachingCounters { /** Total number of already cached bytes. */ - public long alreadyCachedBytes; + public volatile long alreadyCachedBytes; + /** Total number of downloaded bytes. */ + public volatile long downloadedBytes; /** - * Total number of downloaded bytes. - * - *

        {@link #getCached(DataSpec, Cache, CachingCounters)} sets it to the count of the missing - * bytes or to {@link C#LENGTH_UNSET} if {@code dataSpec} is unbounded and content length isn't - * available in the {@code cache}. + * Total number of bytes. This is the sum of already cached, downloaded and missing bytes. If + * the length of the missing bytes is unknown this is set to {@link C#LENGTH_UNSET}. */ - public long downloadedBytes; + public volatile long totalBytes = C.LENGTH_UNSET; } + /** Default buffer size to be used while caching. */ + public static final int DEFAULT_BUFFER_SIZE_BYTES = 128 * 1024; + /** * Generates a cache key out of the given {@link Uri}. * @@ -76,14 +80,34 @@ public final class CacheUtil { public static CachingCounters getCached(DataSpec dataSpec, Cache cache, CachingCounters counters) { try { - return internalCache(dataSpec, cache, null, null, null, 0, counters); + return internalCache(dataSpec, cache, null, null, null, 0, counters, false); } catch (IOException | InterruptedException e) { throw new IllegalStateException(e); } } /** - * Caches the data defined by {@code dataSpec} while skipping already cached data. + * Caches the data defined by {@code dataSpec} while skipping already cached data. Caching stops + * early if end of input is reached. + * + * @param dataSpec Defines the data to be cached. + * @param cache A {@link Cache} to store the data. + * @param upstream A {@link DataSource} for reading data not in the cache. + * @param counters The counters to be set during caching. If not null its values reset to + * zero before using. If null a new {@link CachingCounters} is created and used. + * @return The used {@link CachingCounters} instance. + * @throws IOException If an error occurs reading from the source. + * @throws InterruptedException If the thread was interrupted. + */ + public static CachingCounters cache(DataSpec dataSpec, Cache cache, + DataSource upstream, CachingCounters counters) throws IOException, InterruptedException { + return cache(dataSpec, cache, new CacheDataSource(cache, upstream), + new byte[DEFAULT_BUFFER_SIZE_BYTES], null, 0, counters, false); + } + + /** + * Caches the data defined by {@code dataSpec} while skipping already cached data. Caching stops + * early if end of input is reached and {@code enableEOFException} is false. * * @param dataSpec Defines the data to be cached. * @param cache A {@link Cache} to store the data. @@ -94,17 +118,20 @@ public final class CacheUtil { * @param priority The priority of this task. Used with {@code priorityTaskManager}. * @param counters The counters to be set during caching. If not null its values reset to * zero before using. If null a new {@link CachingCounters} is created and used. + * @param enableEOFException Whether to throw an {@link EOFException} if end of input has been + * reached unexpectedly. * @return The used {@link CachingCounters} instance. * @throws IOException If an error occurs reading from the source. * @throws InterruptedException If the thread was interrupted. */ public static CachingCounters cache(DataSpec dataSpec, Cache cache, CacheDataSource dataSource, byte[] buffer, PriorityTaskManager priorityTaskManager, int priority, - CachingCounters counters) throws IOException, InterruptedException { + CachingCounters counters, boolean enableEOFException) + throws IOException, InterruptedException { Assertions.checkNotNull(dataSource); Assertions.checkNotNull(buffer); return internalCache(dataSpec, cache, dataSource, buffer, priorityTaskManager, priority, - counters); + counters, enableEOFException); } /** @@ -121,21 +148,21 @@ public final class CacheUtil { * @param priority The priority of this task. Used with {@code priorityTaskManager}. * @param counters The counters to be set during caching. If not null its values reset to * zero before using. If null a new {@link CachingCounters} is created and used. + * @param enableEOFException Whether to throw an {@link EOFException} if end of input has been + * reached unexpectedly. * @return The used {@link CachingCounters} instance. * @throws IOException If not dry run and an error occurs reading from the source. * @throws InterruptedException If not dry run and the thread was interrupted. */ private static CachingCounters internalCache(DataSpec dataSpec, Cache cache, CacheDataSource dataSource, byte[] buffer, PriorityTaskManager priorityTaskManager, - int priority, CachingCounters counters) throws IOException, InterruptedException { - long start = dataSpec.position; + int priority, CachingCounters counters, boolean enableEOFException) + throws IOException, InterruptedException { + long start = dataSpec.absoluteStreamPosition; long left = dataSpec.length; String key = getKey(dataSpec); if (left == C.LENGTH_UNSET) { left = cache.getContentLength(key); - if (left == C.LENGTH_UNSET) { - left = Long.MAX_VALUE; - } } if (counters == null) { counters = new CachingCounters(); @@ -143,8 +170,11 @@ public final class CacheUtil { counters.alreadyCachedBytes = 0; counters.downloadedBytes = 0; } - while (left > 0) { - long blockLength = cache.getCachedBytes(key, start, left); + counters.totalBytes = left; + + while (left != 0) { + long blockLength = cache.getCachedBytes(key, start, + left != C.LENGTH_UNSET ? left : Long.MAX_VALUE); // Skip already cached data if (blockLength > 0) { counters.alreadyCachedBytes += blockLength; @@ -152,24 +182,21 @@ public final class CacheUtil { // There is a hole in the cache which is at least "-blockLength" long. blockLength = -blockLength; if (dataSource != null && buffer != null) { - DataSpec subDataSpec = new DataSpec(dataSpec.uri, start, - blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength, key); - long read = readAndDiscard(subDataSpec, dataSource, buffer, priorityTaskManager, - priority); - counters.downloadedBytes += read; + long read = readAndDiscard(dataSpec, start, blockLength, dataSource, buffer, + priorityTaskManager, priority, counters); if (read < blockLength) { - // Reached end of data. + // Reached to the end of the data. + if (enableEOFException && left != C.LENGTH_UNSET) { + throw new EOFException(); + } break; } } else if (blockLength == Long.MAX_VALUE) { - counters.downloadedBytes = C.LENGTH_UNSET; break; - } else { - counters.downloadedBytes += blockLength; } } start += blockLength; - if (left != Long.MAX_VALUE) { + if (left != C.LENGTH_UNSET) { left -= blockLength; } } @@ -179,36 +206,56 @@ public final class CacheUtil { /** * Reads and discards all data specified by the {@code dataSpec}. * - * @param dataSpec Defines the data to be read. + * @param dataSpec Defines the data to be read. {@code absoluteStreamPosition} and {@code length} + * fields are overwritten by the following parameters. + * @param absoluteStreamPosition The absolute position of the data to be read. + * @param length Length of the data to be read, or {@link C#LENGTH_UNSET} if it is unknown. * @param dataSource The {@link DataSource} to read the data from. * @param buffer The buffer to be used while downloading. * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with * caching. * @param priority The priority of this task. + * @param counters The counters to be set during reading. * @return Number of read bytes, or 0 if no data is available because the end of the opened range - * has been reached. + * has been reached. */ - private static long readAndDiscard(DataSpec dataSpec, DataSource dataSource, byte[] buffer, - PriorityTaskManager priorityTaskManager, int priority) - throws IOException, InterruptedException { + private static long readAndDiscard(DataSpec dataSpec, long absoluteStreamPosition, long length, + DataSource dataSource, byte[] buffer, PriorityTaskManager priorityTaskManager, int priority, + CachingCounters counters) throws IOException, InterruptedException { while (true) { if (priorityTaskManager != null) { // Wait for any other thread with higher priority to finish its job. priorityTaskManager.proceed(priority); } try { - dataSource.open(dataSpec); + // 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.postBody, absoluteStreamPosition, + dataSpec.position + absoluteStreamPosition - dataSpec.absoluteStreamPosition, + C.LENGTH_UNSET, dataSpec.key, + dataSpec.flags | DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH); + long resolvedLength = dataSource.open(dataSpec); + if (counters.totalBytes == C.LENGTH_UNSET && resolvedLength != C.LENGTH_UNSET) { + counters.totalBytes = dataSpec.absoluteStreamPosition + resolvedLength; + } long totalRead = 0; - while (true) { + while (totalRead != length) { if (Thread.interrupted()) { throw new InterruptedException(); } - int read = dataSource.read(buffer, 0, buffer.length); + int read = dataSource.read(buffer, 0, + length != C.LENGTH_UNSET ? (int) Math.min(buffer.length, length - totalRead) + : buffer.length); if (read == C.RESULT_END_OF_INPUT) { - return totalRead; + if (counters.totalBytes == C.LENGTH_UNSET) { + counters.totalBytes = dataSpec.absoluteStreamPosition + totalRead; + } + break; } totalRead += read; + counters.downloadedBytes += read; } + return totalRead; } catch (PriorityTaskManager.PriorityTooLowException exception) { // catch and try again } finally { From 076a77e598dc9f32110c4fcbe9fd67cc4a5e023b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 26 Jun 2017 23:41:00 +0100 Subject: [PATCH 171/220] Fix indexing error --- .../google/android/exoplayer2/source/ExtractorMediaPeriod.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 8a35034bb7..189391f711 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -217,7 +217,7 @@ import java.util.Arrays; streamResetFlags[i] = true; // If there's still a chance of avoiding a seek, try and seek within the sample queue. if (!seekRequired) { - SampleQueue sampleQueue = sampleQueues[i]; + SampleQueue sampleQueue = sampleQueues[track]; sampleQueue.rewind(); seekRequired = !sampleQueue.advanceTo(positionUs, true, true) && sampleQueue.getReadIndex() != 0; From 410228acb80f51303fd55572ff7cbf41983e4f39 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 27 Jun 2017 01:05:16 -0700 Subject: [PATCH 172/220] Rename isLast to isFinal in MediaPeriodHolder. This better reflects the purpose of this flag and makes code more readable. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160246573 --- .../exoplayer2/ExoPlayerImplInternal.java | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 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 65dea43d08..90fa3c4f47 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 @@ -453,8 +453,8 @@ import java.io.IOException; releasePeriodHoldersFrom(lastValidPeriodHolder.next); lastValidPeriodHolder.next = null; } - // Update isLast flag. - lastValidPeriodHolder.isLast = isLastPeriod(lastValidPeriodHolder.periodIndex); + // Update isFinal flag. + lastValidPeriodHolder.isFinal = isFinalPeriod(lastValidPeriodHolder.periodIndex); // Handle cases where loadingPeriodHolder or readingPeriodHolder have been removed. if (!seenLoadingPeriodHolder) { loadingPeriodHolder = lastValidPeriodHolder; @@ -570,7 +570,7 @@ import java.io.IOException; if (allRenderersEnded && (playingPeriodDurationUs == C.TIME_UNSET || playingPeriodDurationUs <= playbackInfo.positionUs) - && playingPeriodHolder.isLast) { + && playingPeriodHolder.isFinal) { setState(ExoPlayer.STATE_ENDED); stopRenderers(); } else if (state == ExoPlayer.STATE_BUFFERING) { @@ -916,7 +916,7 @@ import java.io.IOException; ? loadingPeriodHolder.startPositionUs : loadingPeriodHolder.mediaPeriod.getBufferedPositionUs(); if (loadingPeriodBufferedPositionUs == C.TIME_END_OF_SOURCE) { - if (loadingPeriodHolder.isLast) { + if (loadingPeriodHolder.isFinal) { return true; } loadingPeriodBufferedPositionUs = timeline.getPeriod(loadingPeriodHolder.periodIndex, period) @@ -1010,7 +1010,7 @@ import java.io.IOException; } // The current period is in the new timeline. Update the holder and playbackInfo. - periodHolder.setPeriodIndex(periodIndex, isLastPeriod(periodIndex)); + periodHolder.setPeriodIndex(periodIndex, isFinalPeriod(periodIndex)); boolean seenReadingPeriod = periodHolder == readingPeriodHolder; if (periodIndex != playbackInfo.periodIndex) { playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); @@ -1025,7 +1025,7 @@ import java.io.IOException; if (periodIndex != C.INDEX_UNSET && periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) { // The holder is consistent with the new timeline. Update its index and continue. - periodHolder.setPeriodIndex(periodIndex, isLastPeriod(periodIndex)); + periodHolder.setPeriodIndex(periodIndex, isFinalPeriod(periodIndex)); seenReadingPeriod |= (periodHolder == readingPeriodHolder); } else { // The holder is inconsistent with the new timeline. @@ -1093,7 +1093,7 @@ import java.io.IOException; return newPeriodIndex; } - private boolean isLastPeriod(int periodIndex) { + private boolean isFinalPeriod(int periodIndex) { int windowIndex = timeline.getPeriod(periodIndex, period).windowIndex; return !timeline.getWindow(windowIndex, window).isDynamic && timeline.isLastPeriod(periodIndex, period, window, repeatMode); @@ -1229,7 +1229,7 @@ import java.io.IOException; eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); } - if (readingPeriodHolder.isLast) { + if (readingPeriodHolder.isFinal) { for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; @@ -1298,7 +1298,7 @@ import java.io.IOException; newLoadingPeriodIndex = playbackInfo.periodIndex; } else { int loadingPeriodIndex = loadingPeriodHolder.periodIndex; - if (loadingPeriodHolder.isLast || !loadingPeriodHolder.isFullyBuffered() + if (loadingPeriodHolder.isFinal || !loadingPeriodHolder.isFullyBuffered() || timeline.getPeriod(loadingPeriodIndex, period).getDurationUs() == C.TIME_UNSET) { // Either the existing loading period is the last period, or we are not ready to advance to // loading the next period because it hasn't been fully buffered or its duration is unknown. @@ -1313,12 +1313,11 @@ import java.io.IOException; } newLoadingPeriodIndex = timeline.getNextPeriodIndex(loadingPeriodIndex, period, window, repeatMode); - } - - if (newLoadingPeriodIndex == C.INDEX_UNSET) { - // The next period is not available yet. - mediaSource.maybeThrowSourceInfoRefreshError(); - return; + if (newLoadingPeriodIndex == C.INDEX_UNSET) { + // The next period is not available yet. + mediaSource.maybeThrowSourceInfoRefreshError(); + return; + } } long newLoadingPeriodStartPositionUs; @@ -1356,7 +1355,7 @@ import java.io.IOException; : (loadingPeriodHolder.getRendererOffset() + timeline.getPeriod(loadingPeriodHolder.periodIndex, period).getDurationUs()); int holderIndex = loadingPeriodHolder == null ? 0 : loadingPeriodHolder.index + 1; - boolean isLastPeriod = isLastPeriod(newLoadingPeriodIndex); + boolean isLastPeriod = isFinalPeriod(newLoadingPeriodIndex); timeline.getPeriod(newLoadingPeriodIndex, period, true); MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, period.uid, @@ -1508,7 +1507,7 @@ import java.io.IOException; public int periodIndex; public long startPositionUs; - public boolean isLast; + public boolean isFinal; public boolean prepared; public boolean hasEnabledTracks; public MediaPeriodHolder next; @@ -1524,8 +1523,8 @@ import java.io.IOException; public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities, long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl, - MediaSource mediaSource, Object periodUid, int index, int periodIndex, boolean isLastPeriod, - long startPositionUs) { + MediaSource mediaSource, Object periodUid, int index, int periodIndex, + boolean isFinalPeriod, long startPositionUs) { this.renderers = renderers; this.rendererCapabilities = rendererCapabilities; this.rendererPositionOffsetUs = rendererPositionOffsetUs; @@ -1535,7 +1534,7 @@ import java.io.IOException; this.uid = Assertions.checkNotNull(periodUid); this.index = index; this.periodIndex = periodIndex; - this.isLast = isLastPeriod; + this.isFinal = isFinalPeriod; this.startPositionUs = startPositionUs; sampleStreams = new SampleStream[renderers.length]; mayRetainStreamFlags = new boolean[renderers.length]; @@ -1554,9 +1553,9 @@ import java.io.IOException; return rendererPositionOffsetUs - startPositionUs; } - public void setPeriodIndex(int periodIndex, boolean isLast) { + public void setPeriodIndex(int periodIndex, boolean isFinal) { this.periodIndex = periodIndex; - this.isLast = isLast; + this.isFinal = isFinal; } public boolean isFullyBuffered() { From 444dbeb4c405045dd5b51e1f68fa144f1989a3fe Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 27 Jun 2017 01:45:13 -0700 Subject: [PATCH 173/220] Fix NPE in FakeDataSource.close() If open() fails because of the file isn't available then fakeData is null. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160249214 --- .../com/google/android/exoplayer2/testutil/FakeDataSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java index 4d42c3e48e..b3f76391e4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java @@ -199,7 +199,7 @@ public final class FakeDataSource implements DataSource { Assertions.checkState(opened); opened = false; uri = null; - if (currentSegmentIndex < fakeData.segments.size()) { + if (fakeData != null && currentSegmentIndex < fakeData.segments.size()) { Segment current = fakeData.segments.get(currentSegmentIndex); if (current.isErrorSegment() && current.exceptionThrown) { current.exceptionCleared = true; From d4059ecc6501d2b7f42c2c8fa24ecb3bd6bb2832 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 27 Jun 2017 03:41:30 -0700 Subject: [PATCH 174/220] Use period holder indices to determine if a period was seen ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160257503 --- .../exoplayer2/ExoPlayerImplInternal.java | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 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 90fa3c4f47..bb64655c49 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 @@ -430,35 +430,39 @@ import java.io.IOException; private void setRepeatModeInternal(@ExoPlayer.RepeatMode int repeatMode) throws ExoPlaybackException { this.repeatMode = repeatMode; - // Check if all existing period holders match the new period order. + + // Find the last existing period holder that matches the new period order. MediaPeriodHolder lastValidPeriodHolder = playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder; if (lastValidPeriodHolder == null) { return; } - boolean seenReadingPeriodHolder = lastValidPeriodHolder == readingPeriodHolder; - boolean seenLoadingPeriodHolder = lastValidPeriodHolder == loadingPeriodHolder; int nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.periodIndex, period, window, repeatMode); while (lastValidPeriodHolder.next != null && nextPeriodIndex != C.INDEX_UNSET && lastValidPeriodHolder.next.periodIndex == nextPeriodIndex) { lastValidPeriodHolder = lastValidPeriodHolder.next; - seenReadingPeriodHolder |= lastValidPeriodHolder == readingPeriodHolder; - seenLoadingPeriodHolder |= lastValidPeriodHolder == loadingPeriodHolder; nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.periodIndex, period, window, repeatMode); } - // Release all period holder beyond the last one matching the new period order. + + // Release any period holders that don't match the new period order. + int loadingPeriodHolderIndex = loadingPeriodHolder.index; + int readingPeriodHolderIndex = + readingPeriodHolder != null ? readingPeriodHolder.index : C.INDEX_UNSET; if (lastValidPeriodHolder.next != null) { releasePeriodHoldersFrom(lastValidPeriodHolder.next); lastValidPeriodHolder.next = null; } - // Update isFinal flag. lastValidPeriodHolder.isFinal = isFinalPeriod(lastValidPeriodHolder.periodIndex); + // Handle cases where loadingPeriodHolder or readingPeriodHolder have been removed. + boolean seenLoadingPeriodHolder = loadingPeriodHolderIndex <= lastValidPeriodHolder.index; if (!seenLoadingPeriodHolder) { loadingPeriodHolder = lastValidPeriodHolder; } + boolean seenReadingPeriodHolder = readingPeriodHolderIndex != C.INDEX_UNSET + && readingPeriodHolderIndex <= lastValidPeriodHolder.index; if (!seenReadingPeriodHolder && playingPeriodHolder != null) { // Renderers may have read from a period that's been removed. Seek back to the current // position of the playing period to make sure none of the removed period is played. @@ -466,6 +470,7 @@ import java.io.IOException; long newPositionUs = seekToPeriodPosition(playingPeriodIndex, playbackInfo.positionUs); playbackInfo = new PlaybackInfo(playingPeriodIndex, newPositionUs); } + // Restart buffering if playback has ended and repetition is enabled. if (state == ExoPlayer.STATE_ENDED && repeatMode != ExoPlayer.REPEAT_MODE_OFF) { setState(ExoPlayer.STATE_BUFFERING); @@ -1011,7 +1016,6 @@ import java.io.IOException; // The current period is in the new timeline. Update the holder and playbackInfo. periodHolder.setPeriodIndex(periodIndex, isFinalPeriod(periodIndex)); - boolean seenReadingPeriod = periodHolder == readingPeriodHolder; if (periodIndex != playbackInfo.periodIndex) { playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); } @@ -1026,10 +1030,11 @@ import java.io.IOException; && periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) { // The holder is consistent with the new timeline. Update its index and continue. periodHolder.setPeriodIndex(periodIndex, isFinalPeriod(periodIndex)); - seenReadingPeriod |= (periodHolder == readingPeriodHolder); } else { // The holder is inconsistent with the new timeline. - if (!seenReadingPeriod) { + boolean seenReadingPeriodHolder = + readingPeriodHolder != null && readingPeriodHolder.index < periodHolder.index; + if (!seenReadingPeriodHolder) { // Renderers may have read from a period that's been removed. Seek back to the current // position of the playing period to make sure none of the removed period is played. periodIndex = playingPeriodHolder.periodIndex; From 675756d32dff5d9541909f38691ee9fccabc857b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 27 Jun 2017 06:29:29 -0700 Subject: [PATCH 175/220] Create MediaPeriods based on an identifier not an index This will allow MediaSources to provide MediaPeriods that correspond to ad breaks in a timeline period rather than content for a timeline period, in a future change. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160267841 --- .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 11 ++++--- .../android/exoplayer2/ExoPlayerTest.java | 4 +-- .../android/exoplayer2/TimelineTest.java | 2 +- .../exoplayer2/ExoPlayerImplInternal.java | 4 ++- .../source/ClippingMediaSource.java | 4 +-- .../source/ConcatenatingMediaSource.java | 10 +++---- .../source/ExtractorMediaSource.java | 4 +-- .../exoplayer2/source/LoopingMediaSource.java | 6 ++-- .../exoplayer2/source/MediaSource.java | 29 ++++++++++++++++--- .../exoplayer2/source/MergingMediaSource.java | 4 +-- .../source/SingleSampleMediaSource.java | 4 +-- .../source/dash/DashMediaSource.java | 3 +- .../exoplayer2/source/hls/HlsMediaSource.java | 4 +-- .../source/smoothstreaming/SsMediaSource.java | 4 +-- 14 files changed, 60 insertions(+), 33 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 563685e6dc..ea6aaaf01c 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -159,7 +159,8 @@ public final class ImaAdsMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + int index = id.periodIndex; if (timeline.isPeriodAd(index)) { int adBreakIndex = timeline.getAdBreakIndex(index); int adIndexInAdBreak = timeline.getAdIndexInAdBreak(index); @@ -174,13 +175,15 @@ public final class ImaAdsMediaSource implements MediaSource { } MediaSource adBreakMediaSource = adBreakMediaSources[adBreakIndex][adIndexInAdBreak]; - MediaPeriod adBreakMediaPeriod = adBreakMediaSource.createPeriod(0, allocator); + MediaPeriod adBreakMediaPeriod = + adBreakMediaSource.createPeriod(new MediaPeriodId(0), allocator); mediaSourceByMediaPeriod.put(adBreakMediaPeriod, adBreakMediaSource); return adBreakMediaPeriod; } else { long startUs = timeline.getContentStartTimeUs(index); long endUs = timeline.getContentEndTimeUs(index); - MediaPeriod contentMediaPeriod = contentMediaSource.createPeriod(0, allocator); + MediaPeriod contentMediaPeriod = + contentMediaSource.createPeriod(new MediaPeriodId(0), allocator); ClippingMediaPeriod clippingPeriod = new ClippingMediaPeriod(contentMediaPeriod, true); clippingPeriod.setClipping(startUs, endUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE : endUs); mediaSourceByMediaPeriod.put(contentMediaPeriod, contentMediaSource); @@ -445,7 +448,7 @@ public final class ImaAdsMediaSource implements MediaSource { } public void setMediaSource(MediaSource mediaSource) { - mediaPeriod = mediaSource.createPeriod(index, allocator); + mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(index), allocator); if (callback != null) { mediaPeriod.prepare(this, positionUs); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index f8217ebf11..8d137fa71e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -535,8 +535,8 @@ public final class ExoPlayerTest extends TestCase { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator) { - Assertions.checkIndex(index, 0, timeline.getPeriodCount()); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + Assertions.checkIndex(id.periodIndex, 0, timeline.getPeriodCount()); assertTrue(preparedSource); assertFalse(releasedSource); FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java index 4c5439a0dc..15763ae66d 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java @@ -98,7 +98,7 @@ public class TimelineTest extends TestCase { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return null; } 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 bb64655c49..bd0cf558a7 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 @@ -26,6 +26,7 @@ import android.util.Pair; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; 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.SampleStream; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -1543,7 +1544,8 @@ import java.io.IOException; this.startPositionUs = startPositionUs; sampleStreams = new SampleStream[renderers.length]; mayRetainStreamFlags = new boolean[renderers.length]; - mediaPeriod = mediaSource.createPeriod(periodIndex, loadControl.getAllocator()); + mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(periodIndex), + loadControl.getAllocator()); } public long toRendererTime(long periodTimeUs) { 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 60ee198ed6..99a8033589 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 @@ -91,9 +91,9 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste } @Override - public MediaPeriod createPeriod(int index, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod( - mediaSource.createPeriod(index, allocator), enableInitialDiscontinuity); + mediaSource.createPeriod(id, allocator), enableInitialDiscontinuity); mediaPeriods.add(mediaPeriod); mediaPeriod.setClipping(clippingTimeline.startUs, clippingTimeline.endUs); return mediaPeriod; 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 347c6b77b8..cb939fd14a 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 @@ -91,11 +91,11 @@ public final class ConcatenatingMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator) { - int sourceIndex = timeline.getChildIndexByPeriodIndex(index); - int periodIndexInSource = index - timeline.getFirstPeriodIndexInChild(sourceIndex); - MediaPeriod mediaPeriod = - mediaSources[sourceIndex].createPeriod(periodIndexInSource, allocator); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + int sourceIndex = timeline.getChildIndexByPeriodIndex(id.periodIndex); + MediaPeriodId periodIdInSource = + new MediaPeriodId(id.periodIndex - timeline.getFirstPeriodIndexInChild(sourceIndex)); + MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIdInSource, allocator); sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex); return mediaPeriod; } 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 cd77146df3..1749e6abf2 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 @@ -165,8 +165,8 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List } @Override - public MediaPeriod createPeriod(int index, Allocator allocator) { - Assertions.checkArgument(index == 0); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + Assertions.checkArgument(id.periodIndex == 0); return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(), extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener, this, allocator, customCacheKey, continueLoadingCheckIntervalBytes); 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 c5f4779217..da2593ba15 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 @@ -76,10 +76,10 @@ public final class LoopingMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return loopCount != Integer.MAX_VALUE - ? childSource.createPeriod(index % childPeriodCount, allocator) - : childSource.createPeriod(index, allocator); + ? childSource.createPeriod(new MediaPeriodId(id.periodIndex % childPeriodCount), allocator) + : childSource.createPeriod(id, allocator); } @Override 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 52cb4540bd..d696b43dd7 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 @@ -41,6 +41,27 @@ public interface MediaSource { } + /** + * Identifier for a {@link MediaPeriod}. + */ + final class MediaPeriodId { + + /** + * The timeline period index. + */ + public final int periodIndex; + + /** + * Creates a media period identifier for the specified period in the timeline. + * + * @param periodIndex The timeline period index. + */ + public MediaPeriodId(int periodIndex) { + this.periodIndex = periodIndex; + } + + } + /** * Starts preparation of the source. * @@ -59,15 +80,15 @@ public interface MediaSource { void maybeThrowSourceInfoRefreshError() throws IOException; /** - * Returns a new {@link MediaPeriod} corresponding to the period at the specified {@code index}. - * This method may be called multiple times with the same index without an intervening call to + * Returns a new {@link MediaPeriod} identified by {@code periodId}. This method may be called + * multiple times with the same period identifier without an intervening call to * {@link #releasePeriod(MediaPeriod)}. * - * @param index The index of the period. + * @param id The identifier of the period. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @return A new {@link MediaPeriod}. */ - MediaPeriod createPeriod(int index, Allocator allocator); + MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator); /** * Releases the period. 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 421a05adc2..642752b35b 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 @@ -116,10 +116,10 @@ public final class MergingMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { MediaPeriod[] periods = new MediaPeriod[mediaSources.length]; for (int i = 0; i < periods.length; i++) { - periods[i] = mediaSources[i].createPeriod(index, allocator); + periods[i] = mediaSources[i].createPeriod(id, allocator); } return new MergingMediaPeriod(periods); } 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 7544176c54..99bc60d6fb 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 @@ -95,8 +95,8 @@ public final class SingleSampleMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator) { - Assertions.checkArgument(index == 0); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + Assertions.checkArgument(id.periodIndex == 0); return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount, eventHandler, eventListener, eventSourceId); } 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 82503673e5..f1d5ad96fa 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 @@ -281,7 +281,8 @@ public final class DashMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int periodIndex, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId periodId, Allocator allocator) { + int periodIndex = periodId.periodIndex; EventDispatcher periodEventDispatcher = eventDispatcher.copyWithMediaTimeOffsetMs( manifest.getPeriod(periodIndex).startMs); DashMediaPeriod mediaPeriod = new DashMediaPeriod(firstPeriodId + periodIndex, manifest, 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 5839a4af38..0c97fb5ecc 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 @@ -88,8 +88,8 @@ public final class HlsMediaSource implements MediaSource, } @Override - public MediaPeriod createPeriod(int index, Allocator allocator) { - Assertions.checkArgument(index == 0); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + Assertions.checkArgument(id.periodIndex == 0); return new HlsMediaPeriod(playlistTracker, dataSourceFactory, minLoadableRetryCount, eventDispatcher, allocator); } 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 855b922135..d868d1fb9e 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 @@ -222,8 +222,8 @@ public final class SsMediaSource implements MediaSource, } @Override - public MediaPeriod createPeriod(int index, Allocator allocator) { - Assertions.checkArgument(index == 0); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + Assertions.checkArgument(id.periodIndex == 0); SsMediaPeriod period = new SsMediaPeriod(manifest, chunkSourceFactory, minLoadableRetryCount, eventDispatcher, manifestLoaderErrorThrower, allocator); mediaPeriods.add(period); From 1dcad4d1733cf0f64be02d1bfaa184612bcfcef0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 27 Jun 2017 07:17:46 -0700 Subject: [PATCH 176/220] Store a MediaPeriodId in PlaybackInfo ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160271631 --- .../android/exoplayer2/ExoPlayerImpl.java | 8 ++++---- .../exoplayer2/ExoPlayerImplInternal.java | 20 +++++++++++-------- .../exoplayer2/source/MediaSource.java | 16 +++++++++++++++ 3 files changed, 32 insertions(+), 12 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 680cbe71ff..a540b18974 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 @@ -281,7 +281,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingPeriodIndex; } else { - return playbackInfo.periodIndex; + return playbackInfo.periodId.periodIndex; } } @@ -290,7 +290,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingWindowIndex; } else { - return timeline.getPeriod(playbackInfo.periodIndex, period).windowIndex; + return timeline.getPeriod(playbackInfo.periodId.periodIndex, period).windowIndex; } } @@ -307,7 +307,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingWindowPositionMs; } else { - timeline.getPeriod(playbackInfo.periodIndex, period); + timeline.getPeriod(playbackInfo.periodId.periodIndex, period); return period.getPositionInWindowMs() + C.usToMs(playbackInfo.positionUs); } } @@ -318,7 +318,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingWindowPositionMs; } else { - timeline.getPeriod(playbackInfo.periodIndex, period); + timeline.getPeriod(playbackInfo.periodId.periodIndex, period); return period.getPositionInWindowMs() + C.usToMs(playbackInfo.bufferedPositionUs); } } 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 bd0cf558a7..1a79c50274 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 @@ -50,21 +50,25 @@ import java.io.IOException; */ public static final class PlaybackInfo { - public final int periodIndex; + public final MediaPeriodId periodId; public final long startPositionUs; public volatile long positionUs; public volatile long bufferedPositionUs; public PlaybackInfo(int periodIndex, long startPositionUs) { - this.periodIndex = periodIndex; + this(new MediaPeriodId(periodIndex), startPositionUs); + } + + public PlaybackInfo(MediaPeriodId periodId, long startPositionUs) { + this.periodId = periodId; this.startPositionUs = startPositionUs; positionUs = startPositionUs; bufferedPositionUs = startPositionUs; } - public PlaybackInfo copyWithPeriodIndex(int periodIndex) { - PlaybackInfo playbackInfo = new PlaybackInfo(periodIndex, startPositionUs); + public PlaybackInfo copyWithPeriodId(MediaPeriodId periodId) { + PlaybackInfo playbackInfo = new PlaybackInfo(periodId.periodIndex, startPositionUs); playbackInfo.positionUs = positionUs; playbackInfo.bufferedPositionUs = bufferedPositionUs; return playbackInfo; @@ -654,7 +658,7 @@ import java.io.IOException; long periodPositionUs = periodPosition.second; try { - if (periodIndex == playbackInfo.periodIndex + if (periodIndex == playbackInfo.periodId.periodIndex && ((periodPositionUs / 1000) == (playbackInfo.positionUs / 1000))) { // Seek position equals the current position. Do nothing. return; @@ -1017,8 +1021,8 @@ import java.io.IOException; // The current period is in the new timeline. Update the holder and playbackInfo. periodHolder.setPeriodIndex(periodIndex, isFinalPeriod(periodIndex)); - if (periodIndex != playbackInfo.periodIndex) { - playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); + if (periodIndex != playbackInfo.periodId.periodIndex) { + playbackInfo = playbackInfo.copyWithPeriodId(new MediaPeriodId(periodIndex)); } // If there are subsequent holders, update the index for each of them. If we find a holder @@ -1301,7 +1305,7 @@ import java.io.IOException; private void maybeUpdateLoadingPeriod() throws IOException { int newLoadingPeriodIndex; if (loadingPeriodHolder == null) { - newLoadingPeriodIndex = playbackInfo.periodIndex; + newLoadingPeriodIndex = playbackInfo.periodId.periodIndex; } else { int loadingPeriodIndex = loadingPeriodHolder.periodIndex; if (loadingPeriodHolder.isFinal || !loadingPeriodHolder.isFullyBuffered() 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 d696b43dd7..77e3b6be65 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source; import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; @@ -51,13 +52,28 @@ public interface MediaSource { */ public final int periodIndex; + /** + * If the media period is in an ad break, the index of the ad break in the period. + * {@link C#INDEX_UNSET} otherwise. + */ + public final int adBreakIndex; + + /** + * If the media period is in an ad break, the index of the ad in its ad break in the period. + * {@link C#INDEX_UNSET} otherwise. + */ + public final int adIndexInAdBreak; + /** * Creates a media period identifier for the specified period in the timeline. * * @param periodIndex The timeline period index. */ public MediaPeriodId(int periodIndex) { + // TODO: Allow creation of MediaPeriodIds for ad breaks. this.periodIndex = periodIndex; + adBreakIndex = C.INDEX_UNSET; + adIndexInAdBreak = C.INDEX_UNSET; } } From 2a8eb5a2a11b8e51120e3aca3884c95c4c144e29 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 27 Jun 2017 07:23:02 -0700 Subject: [PATCH 177/220] Make it easier to use ExoPlayer modules in other projects It's currently difficult to use ExoPlayer modules in other gradle projects because they rely on constants and dependencies defined in our own top level gradle file. This change moves the constants into a separate file referenced directly from each module. It also removes the need for the top level gradle file to declare a dependency on com.novoda:bintray-release. This is now only needed if "exoplayerPublishEnabled = true" is specified. Issue: #2851 Issue: #2974 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160272072 --- build.gradle | 24 +----------------------- demo/build.gradle | 1 + extensions/cronet/build.gradle | 1 + extensions/ffmpeg/build.gradle | 1 + extensions/flac/build.gradle | 1 + extensions/gvr/build.gradle | 1 + extensions/ima/build.gradle | 1 + extensions/okhttp/build.gradle | 1 + extensions/opus/build.gradle | 1 + extensions/vp9/build.gradle | 1 + library/all/build.gradle | 1 + library/core/build.gradle | 1 + library/dash/build.gradle | 1 + library/hls/build.gradle | 1 + library/smoothstreaming/build.gradle | 1 + library/ui/build.gradle | 1 + playbacktests/build.gradle | 1 + publish.gradle | 28 ++++++++++++++++++---------- testutils/build.gradle | 1 + version_constants.gradle | 28 ++++++++++++++++++++++++++++ 20 files changed, 64 insertions(+), 33 deletions(-) create mode 100644 version_constants.gradle diff --git a/build.gradle b/build.gradle index 4f18e7c801..981bab596c 100644 --- a/build.gradle +++ b/build.gradle @@ -33,23 +33,7 @@ allprojects { jcenter() } project.ext { - // Important: ExoPlayer specifies a minSdkVersion of 9 because various - // components provided by the library may be of use on older devices. - // However, please note that the core media playback functionality - // provided by the library requires API level 16 or greater. - minSdkVersion = 9 - compileSdkVersion = 25 - targetSdkVersion = 25 - buildToolsVersion = '25' - testSupportLibraryVersion = '0.5' - supportLibraryVersion = '25.3.1' - dexmakerVersion = '1.2' - mockitoVersion = '1.9.5' - releaseRepoName = getBintrayRepo() - releaseUserOrg = 'google' - releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.4.2' - releaseWebsite = 'https://github.com/google/ExoPlayer' + exoplayerPublishEnabled = true } if (it.hasProperty('externalBuildDir')) { if (!new File(externalBuildDir).isAbsolute()) { @@ -59,10 +43,4 @@ allprojects { } } -def getBintrayRepo() { - boolean publicRepo = hasProperty('publicRepo') && - property('publicRepo').toBoolean() - return publicRepo ? 'exoplayer' : 'exoplayer-test' -} - apply from: 'javadoc_combined.gradle' diff --git a/demo/build.gradle b/demo/build.gradle index 939c5ac93d..4d930b8a78 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -11,6 +11,7 @@ // 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: '../version_constants.gradle' apply plugin: 'com.android.application' android { diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 5611817b2e..199cee2a71 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -11,6 +11,7 @@ // 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: '../../version_constants.gradle' apply plugin: 'com.android.library' android { diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index 0eddd017a4..57e1c25bc0 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -11,6 +11,7 @@ // 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: '../../version_constants.gradle' apply plugin: 'com.android.library' android { diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 4a6b8e0e5a..ebe5d1d796 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -11,6 +11,7 @@ // 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: '../../version_constants.gradle' apply plugin: 'com.android.library' android { diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index e15c8b1ad8..d21912a686 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -11,6 +11,7 @@ // 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: '../../version_constants.gradle' apply plugin: 'com.android.library' android { diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 4ba26cc244..90ca98ab84 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -1,3 +1,4 @@ +apply from: '../../version_constants.gradle' apply plugin: 'com.android.library' android { diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index f47f1a8556..7f3f3d2e1a 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -11,6 +11,7 @@ // 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: '../../version_constants.gradle' apply plugin: 'com.android.library' android { diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index 31d5450fdd..7c86d994d9 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -11,6 +11,7 @@ // 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: '../../version_constants.gradle' apply plugin: 'com.android.library' android { diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index 5068586a4a..8f2947439b 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -11,6 +11,7 @@ // 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: '../../version_constants.gradle' apply plugin: 'com.android.library' android { diff --git a/library/all/build.gradle b/library/all/build.gradle index 63943ada77..270d92ce33 100644 --- a/library/all/build.gradle +++ b/library/all/build.gradle @@ -11,6 +11,7 @@ // 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: '../../version_constants.gradle' apply plugin: 'com.android.library' android { diff --git a/library/core/build.gradle b/library/core/build.gradle index ad1c150fe7..5d9ece71b6 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. apply plugin: 'com.android.library' +apply from: '../../version_constants.gradle' android { compileSdkVersion project.ext.compileSdkVersion diff --git a/library/dash/build.gradle b/library/dash/build.gradle index 36d3edfbae..e1598f783f 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -11,6 +11,7 @@ // 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: '../../version_constants.gradle' apply plugin: 'com.android.library' android { diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 47b5758b1d..140dafc237 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -11,6 +11,7 @@ // 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: '../../version_constants.gradle' apply plugin: 'com.android.library' android { diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index 28ebd74758..dd51dc13aa 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -11,6 +11,7 @@ // 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: '../../version_constants.gradle' apply plugin: 'com.android.library' android { diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 96dcd52655..be600ec126 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -11,6 +11,7 @@ // 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: '../../version_constants.gradle' apply plugin: 'com.android.library' android { diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index 199077f2b2..217d913853 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -11,6 +11,7 @@ // 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: '../version_constants.gradle' apply plugin: 'com.android.library' android { diff --git a/publish.gradle b/publish.gradle index 17214959ab..ca1a2cfd8b 100644 --- a/publish.gradle +++ b/publish.gradle @@ -11,14 +11,22 @@ // 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 plugin: 'bintray-release' - -publish { - artifactId = releaseArtifact - description = releaseDescription - repoName = releaseRepoName - userOrg = releaseUserOrg - groupId = releaseGroupId - version = releaseVersion - website = releaseWebsite +if (project.ext.has("exoplayerPublishEnabled") + && project.ext.exoplayerPublishEnabled) { + apply plugin: 'bintray-release' + publish { + artifactId = releaseArtifact + description = releaseDescription + version = releaseVersion + repoName = getBintrayRepo() + userOrg = 'google' + groupId = 'com.google.android.exoplayer' + website = 'https://github.com/google/ExoPlayer' + } +} + +def getBintrayRepo() { + boolean publicRepo = hasProperty('publicRepo') && + property('publicRepo').toBoolean() + return publicRepo ? 'exoplayer' : 'exoplayer-test' } diff --git a/testutils/build.gradle b/testutils/build.gradle index 5fea76f9c3..801bd36298 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -11,6 +11,7 @@ // 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: '../version_constants.gradle' apply plugin: 'com.android.library' android { diff --git a/version_constants.gradle b/version_constants.gradle new file mode 100644 index 0000000000..9f69263c9e --- /dev/null +++ b/version_constants.gradle @@ -0,0 +1,28 @@ +// 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. +ext { + // Important: ExoPlayer specifies a minSdkVersion of 9 because various + // components provided by the library may be of use on older devices. + // However, please note that the core media playback functionality provided + // by the library requires API level 16 or greater. + minSdkVersion = 9 + compileSdkVersion = 25 + targetSdkVersion = 25 + buildToolsVersion = '25' + testSupportLibraryVersion = '0.5' + supportLibraryVersion = '25.3.1' + dexmakerVersion = '1.2' + mockitoVersion = '1.9.5' + releaseVersion = 'r2.4.2' +} From 26b32f60789c571a070da01d59449d02647736e0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 27 Jun 2017 07:46:33 -0700 Subject: [PATCH 178/220] Move getPeriodPosition methods to Timeline ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160273929 --- .../exoplayer2/ExoPlayerImplInternal.java | 54 +++---------------- .../google/android/exoplayer2/Timeline.java | 47 ++++++++++++++++ 2 files changed, 53 insertions(+), 48 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 1a79c50274..1331b8b0cd 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 @@ -1128,7 +1128,7 @@ import java.io.IOException; // Map the SeekPosition to a position in the corresponding timeline. Pair periodPosition; try { - periodPosition = getPeriodPosition(seekTimeline, seekPosition.windowIndex, + periodPosition = seekTimeline.getPeriodPosition(window, period, seekPosition.windowIndex, seekPosition.windowPositionUs); } catch (IndexOutOfBoundsException e) { // The window index of the seek position was outside the bounds of the timeline. @@ -1157,53 +1157,11 @@ import java.io.IOException; } /** - * Calls {@link #getPeriodPosition(Timeline, int, long)} using the current timeline. + * Calls {@link Timeline#getPeriodPosition(Timeline.Window, Timeline.Period, int, long)} using the + * current timeline. */ private Pair getPeriodPosition(int windowIndex, long windowPositionUs) { - return getPeriodPosition(timeline, windowIndex, windowPositionUs); - } - - /** - * Calls {@link #getPeriodPosition(Timeline, int, long, long)} with a zero default position - * projection. - */ - private Pair getPeriodPosition(Timeline timeline, int windowIndex, - long windowPositionUs) { - return getPeriodPosition(timeline, windowIndex, windowPositionUs, 0); - } - - /** - * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs). - * - * @param timeline The timeline containing the window. - * @param windowIndex The window index. - * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default - * start position. - * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the - * duration into the future by which the window's position should be projected. - * @return The corresponding (periodIndex, periodPositionUs), or null if {@code #windowPositionUs} - * is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's - * position could not be projected by {@code defaultPositionProjectionUs}. - */ - private Pair getPeriodPosition(Timeline timeline, int windowIndex, - long windowPositionUs, long defaultPositionProjectionUs) { - Assertions.checkIndex(windowIndex, 0, timeline.getWindowCount()); - timeline.getWindow(windowIndex, window, false, defaultPositionProjectionUs); - if (windowPositionUs == C.TIME_UNSET) { - windowPositionUs = window.getDefaultPositionUs(); - if (windowPositionUs == C.TIME_UNSET) { - return null; - } - } - int periodIndex = window.firstPeriodIndex; - long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs; - long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs(); - while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs - && periodIndex < window.lastPeriodIndex) { - periodPositionUs -= periodDurationUs; - periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs(); - } - return Pair.create(periodIndex, periodPositionUs); + return timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); } private void updatePeriods() throws ExoPlaybackException, IOException { @@ -1349,8 +1307,8 @@ import java.io.IOException; long defaultPositionProjectionUs = loadingPeriodHolder.getRendererOffset() + timeline.getPeriod(loadingPeriodHolder.periodIndex, period).getDurationUs() - rendererPositionUs; - Pair defaultPosition = getPeriodPosition(timeline, newLoadingWindowIndex, - C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs)); + Pair defaultPosition = timeline.getPeriodPosition(window, period, + newLoadingWindowIndex, C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs)); if (defaultPosition == null) { return; } 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 ec2f57685f..6b1e2aba53 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 @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2; +import android.util.Pair; +import com.google.android.exoplayer2.util.Assertions; + /** * A representation of media currently available for playback. *

        @@ -521,6 +524,50 @@ public abstract class Timeline { return getPeriod(periodIndex, period, false); } + /** + * Calls {@link #getPeriodPosition(Window, Period, int, long, long)} with a zero default position + * projection. + */ + public final Pair getPeriodPosition(Window window, Period period, int windowIndex, + long windowPositionUs) { + return getPeriodPosition(window, period, windowIndex, windowPositionUs, 0); + } + + /** + * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs). + * + * @param window A {@link Window} that may be overwritten. + * @param period A {@link Period} that may be overwritten. + * @param windowIndex The window index. + * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default + * start position. + * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the + * duration into the future by which the window's position should be projected. + * @return The corresponding (periodIndex, periodPositionUs), or null if {@code #windowPositionUs} + * is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's + * position could not be projected by {@code defaultPositionProjectionUs}. + */ + public final Pair getPeriodPosition(Window window, Period period, int windowIndex, + long windowPositionUs, long defaultPositionProjectionUs) { + Assertions.checkIndex(windowIndex, 0, getWindowCount()); + getWindow(windowIndex, window, false, defaultPositionProjectionUs); + if (windowPositionUs == C.TIME_UNSET) { + windowPositionUs = window.getDefaultPositionUs(); + if (windowPositionUs == C.TIME_UNSET) { + return null; + } + } + int periodIndex = window.firstPeriodIndex; + long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs; + long periodDurationUs = getPeriod(periodIndex, period).getDurationUs(); + while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs + && periodIndex < window.lastPeriodIndex) { + periodPositionUs -= periodDurationUs; + periodDurationUs = getPeriod(++periodIndex, period).getDurationUs(); + } + return Pair.create(periodIndex, periodPositionUs); + } + /** * Populates a {@link Period} with data for the period at the specified index. * From 4a59c7cf409a3c9caf01217eeba74adb41ad30d2 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 27 Jun 2017 08:27:36 -0700 Subject: [PATCH 179/220] Make it easier to use ExoPlayer modules in other projects II With this change, it becomes possible to depend on ExoPlayer locally in settings.gradle by doing: gradle.ext.exoplayerRoot = 'path/to/exoplayer/root' apply from: new File(gradle.ext.exoplayerRoot, 'core_settings.gradle') You can optionally add a prefix onto ExoPlayer's module names by adding: gradle.ext.exoplayerModulePrefix = 'prefix' Issue: #2851 Issue: #2974 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160277967 --- version_constants.gradle => constants.gradle | 6 ++- core_settings.gradle | 54 ++++++++++++++++++++ demo/build.gradle | 22 ++++---- extensions/cronet/build.gradle | 6 +-- extensions/ffmpeg/build.gradle | 4 +- extensions/flac/build.gradle | 6 +-- extensions/gvr/build.gradle | 4 +- extensions/ima/build.gradle | 6 +-- extensions/okhttp/build.gradle | 4 +- extensions/opus/build.gradle | 4 +- extensions/vp9/build.gradle | 4 +- library/all/build.gradle | 12 ++--- library/core/build.gradle | 2 +- library/dash/build.gradle | 6 +-- library/hls/build.gradle | 4 +- library/smoothstreaming/build.gradle | 6 +-- library/ui/build.gradle | 4 +- playbacktests/build.gradle | 10 ++-- settings.gradle | 46 +++++------------ testutils/build.gradle | 4 +- 20 files changed, 125 insertions(+), 89 deletions(-) rename version_constants.gradle => constants.gradle (88%) create mode 100644 core_settings.gradle diff --git a/version_constants.gradle b/constants.gradle similarity index 88% rename from version_constants.gradle rename to constants.gradle index 9f69263c9e..95221a106f 100644 --- a/version_constants.gradle +++ b/constants.gradle @@ -11,7 +11,7 @@ // 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. -ext { +project.ext { // Important: ExoPlayer specifies a minSdkVersion of 9 because various // components provided by the library may be of use on older devices. // However, please note that the core media playback functionality provided @@ -25,4 +25,8 @@ ext { dexmakerVersion = '1.2' mockitoVersion = '1.9.5' releaseVersion = 'r2.4.2' + modulePrefix = ':'; + if (gradle.ext.has('exoplayerModulePrefix')) { + modulePrefix += gradle.ext.exoplayerModulePrefix + } } diff --git a/core_settings.gradle b/core_settings.gradle new file mode 100644 index 0000000000..abf5422b04 --- /dev/null +++ b/core_settings.gradle @@ -0,0 +1,54 @@ +// 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. +def rootDir = gradle.ext.exoplayerRoot +def modulePrefix = ':' +if (gradle.ext.has('exoplayerModulePrefix')) { + modulePrefix += gradle.ext.exoplayerModulePrefix +} + +include modulePrefix + 'library' +include modulePrefix + 'library-core' +include modulePrefix + 'library-dash' +include modulePrefix + 'library-hls' +include modulePrefix + 'library-smoothstreaming' +include modulePrefix + 'library-ui' +include modulePrefix + 'testutils' +include modulePrefix + 'extension-ffmpeg' +include modulePrefix + 'extension-flac' +include modulePrefix + 'extension-gvr' +include modulePrefix + 'extension-ima' +include modulePrefix + 'extension-okhttp' +include modulePrefix + 'extension-opus' +include modulePrefix + 'extension-vp9' + +project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') +project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core') +project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/dash') +project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls') +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 + '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') +project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima') +project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp') +project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus') +project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9') + +if (gradle.ext.has('exoplayerIncludeCronetExtension') + && gradle.ext.exoplayerIncludeCronetExtension) { + include modulePrefix + 'extension-cronet' + project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet') +} diff --git a/demo/build.gradle b/demo/build.gradle index 4d930b8a78..7eea25478f 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -11,7 +11,7 @@ // 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: '../version_constants.gradle' +apply from: '../constants.gradle' apply plugin: 'com.android.application' android { @@ -46,14 +46,14 @@ android { } dependencies { - compile project(':library-core') - compile project(':library-dash') - compile project(':library-hls') - compile project(':library-smoothstreaming') - compile project(':library-ui') - withExtensionsCompile project(path: ':extension-ffmpeg') - withExtensionsCompile project(path: ':extension-flac') - withExtensionsCompile project(path: ':extension-ima') - withExtensionsCompile project(path: ':extension-opus') - withExtensionsCompile project(path: ':extension-vp9') + compile project(modulePrefix + 'library-core') + compile project(modulePrefix + 'library-dash') + compile project(modulePrefix + 'library-hls') + compile project(modulePrefix + 'library-smoothstreaming') + compile project(modulePrefix + 'library-ui') + withExtensionsCompile project(path: modulePrefix + 'extension-ffmpeg') + withExtensionsCompile project(path: modulePrefix + 'extension-flac') + withExtensionsCompile project(path: modulePrefix + 'extension-ima') + withExtensionsCompile project(path: modulePrefix + 'extension-opus') + withExtensionsCompile project(path: modulePrefix + 'extension-vp9') } diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 199cee2a71..930a53c7c5 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -11,7 +11,7 @@ // 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: '../../version_constants.gradle' +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -30,11 +30,11 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') compile files('libs/cronet_api.jar') compile files('libs/cronet_impl_common_java.jar') compile files('libs/cronet_impl_native_java.jar') - androidTestCompile project(':library') + androidTestCompile project(modulePrefix + 'library') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index 57e1c25bc0..9820818f3e 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -11,7 +11,7 @@ // 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: '../../version_constants.gradle' +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -31,7 +31,7 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') } ext { diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index ebe5d1d796..4d840d34ac 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -11,7 +11,7 @@ // 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: '../../version_constants.gradle' +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -31,8 +31,8 @@ android { } dependencies { - compile project(':library-core') - androidTestCompile project(':testutils') + compile project(modulePrefix + 'library-core') + androidTestCompile project(modulePrefix + 'testutils') } ext { diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index d21912a686..66665576bb 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -11,7 +11,7 @@ // 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: '../../version_constants.gradle' +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -25,7 +25,7 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') compile 'com.google.vr:sdk-audio:1.60.1' } diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 90ca98ab84..3f95fcd414 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -1,4 +1,4 @@ -apply from: '../../version_constants.gradle' +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -13,7 +13,7 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.6.0' compile 'com.google.android.gms:play-services-ads:10.2.4' @@ -28,7 +28,7 @@ dependencies { // will become unnecessary when the support-v4 dependency in the chain above // has been updated to 24.2.0 or later. compile 'com.android.support:support-v4:' + supportLibraryVersion - androidTestCompile project(':library') + androidTestCompile project(modulePrefix + 'library') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 7f3f3d2e1a..0aba07d118 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -11,7 +11,7 @@ // 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: '../../version_constants.gradle' +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -30,7 +30,7 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') compile('com.squareup.okhttp3:okhttp:3.6.0') { exclude group: 'org.json' } diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index 7c86d994d9..41b428070f 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -11,7 +11,7 @@ // 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: '../../version_constants.gradle' +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -31,7 +31,7 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') } ext { diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index 8f2947439b..de6dc65f74 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -11,7 +11,7 @@ // 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: '../../version_constants.gradle' +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -31,7 +31,7 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') } ext { diff --git a/library/all/build.gradle b/library/all/build.gradle index 270d92ce33..79ed9c747b 100644 --- a/library/all/build.gradle +++ b/library/all/build.gradle @@ -11,7 +11,7 @@ // 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: '../../version_constants.gradle' +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -25,11 +25,11 @@ android { } dependencies { - compile project(':library-core') - compile project(':library-dash') - compile project(':library-hls') - compile project(':library-smoothstreaming') - compile project(':library-ui') + compile project(modulePrefix + 'library-core') + compile project(modulePrefix + 'library-dash') + compile project(modulePrefix + 'library-hls') + compile project(modulePrefix + 'library-smoothstreaming') + compile project(modulePrefix + 'library-ui') } ext { diff --git a/library/core/build.gradle b/library/core/build.gradle index 5d9ece71b6..65a7353607 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. apply plugin: 'com.android.library' -apply from: '../../version_constants.gradle' +apply from: '../../constants.gradle' android { compileSdkVersion project.ext.compileSdkVersion diff --git a/library/dash/build.gradle b/library/dash/build.gradle index e1598f783f..aa8031467e 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -11,7 +11,7 @@ // 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: '../../version_constants.gradle' +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -31,10 +31,10 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion compile 'com.android.support:support-core-utils:' + supportLibraryVersion - androidTestCompile project(':testutils') + androidTestCompile project(modulePrefix + 'testutils') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 140dafc237..77680569f0 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -11,7 +11,7 @@ // 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: '../../version_constants.gradle' +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -31,7 +31,7 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index dd51dc13aa..b5f918075f 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -11,7 +11,7 @@ // 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: '../../version_constants.gradle' +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -31,9 +31,9 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion - androidTestCompile project(':testutils') + androidTestCompile project(modulePrefix + 'testutils') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion diff --git a/library/ui/build.gradle b/library/ui/build.gradle index be600ec126..c036bc9819 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -11,7 +11,7 @@ // 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: '../../version_constants.gradle' +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -31,7 +31,7 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion } diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index 217d913853..6cd56868f9 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -11,7 +11,7 @@ // 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: '../version_constants.gradle' +apply from: '../constants.gradle' apply plugin: 'com.android.library' android { @@ -25,8 +25,8 @@ android { } dependencies { - androidTestCompile project(':library-core') - androidTestCompile project(':library-dash') - androidTestCompile project(':library-hls') - androidTestCompile project(':testutils') + androidTestCompile project(modulePrefix + 'library-core') + androidTestCompile project(modulePrefix + 'library-dash') + androidTestCompile project(modulePrefix + 'library-hls') + androidTestCompile project(modulePrefix + 'testutils') } diff --git a/settings.gradle b/settings.gradle index d50cb9d3dd..fb31055f5e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,38 +11,16 @@ // 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. -include ':library' -include ':library-core' -include ':library-dash' -include ':library-hls' -include ':library-smoothstreaming' -include ':library-ui' -include ':testutils' -include ':demo' -include ':playbacktests' -include ':extension-ffmpeg' -include ':extension-flac' -include ':extension-gvr' -include ':extension-ima' -include ':extension-okhttp' -include ':extension-opus' -include ':extension-vp9' -// Uncomment the following line to use the Cronet Extension. -// include ':extension-cronet' +gradle.ext.exoplayerRoot = settingsDir -project(':library').projectDir = new File(settingsDir, 'library/all') -project(':library-core').projectDir = new File(settingsDir, 'library/core') -project(':library-dash').projectDir = new File(settingsDir, 'library/dash') -project(':library-hls').projectDir = new File(settingsDir, 'library/hls') -project(':library-smoothstreaming').projectDir = new File(settingsDir, 'library/smoothstreaming') -project(':library-ui').projectDir = new File(settingsDir, 'library/ui') -project(':extension-ffmpeg').projectDir = new File(settingsDir, 'extensions/ffmpeg') -project(':extension-flac').projectDir = new File(settingsDir, 'extensions/flac') -project(':extension-gvr').projectDir = new File(settingsDir, 'extensions/gvr') -project(':extension-ima').projectDir = new File(settingsDir, 'extensions/ima') -project(':extension-okhttp').projectDir = new File(settingsDir, 'extensions/okhttp') -project(':extension-opus').projectDir = new File(settingsDir, 'extensions/opus') -project(':extension-vp9').projectDir = new File(settingsDir, 'extensions/vp9') -// Uncomment the following line to use the Cronet Extension. -// See extensions/cronet/README.md for details. -// project(':extension-cronet').projectDir = new File(settingsDir, 'extensions/cronet') +def modulePrefix = ':' +if (gradle.ext.has('exoplayerModulePrefix')) { + modulePrefix += gradle.ext.exoplayerModulePrefix +} + +include modulePrefix + 'demo' +include modulePrefix + 'playbacktests' +project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demo') +project(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests') + +apply from: 'core_settings.gradle' diff --git a/testutils/build.gradle b/testutils/build.gradle index 801bd36298..db8462b1fd 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -11,7 +11,7 @@ // 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: '../version_constants.gradle' +apply from: '../constants.gradle' apply plugin: 'com.android.library' android { @@ -25,6 +25,6 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') compile 'org.mockito:mockito-core:' + mockitoVersion } From efb367b417564a258a69140209e43f620c87537c Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 27 Jun 2017 09:38:31 -0700 Subject: [PATCH 180/220] Add URLs EXT-X-STREAM-INF uris only once This prevents ExoPlayer from thinking there are many more video tracks than there actually are. And will prevent downloading multiple times the same rendition once offline support for HLS is added. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160285777 --- .../source/hls/playlist/HlsPlaylistParser.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index ba9bd50194..09d6fcfa18 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -30,6 +30,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Queue; @@ -176,6 +177,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variantUrls = new HashSet<>(); ArrayList variants = new ArrayList<>(); ArrayList audios = new ArrayList<>(); ArrayList subtitles = new ArrayList<>(); @@ -260,11 +262,13 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Tue, 27 Jun 2017 10:11:26 -0700 Subject: [PATCH 181/220] Update READMEs with new local build instructions Issue: #2851 Issue: #2974 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160290097 --- README.md | 38 +++++++++++++++++++++++++++++++++++++ extensions/cronet/README.md | 19 +++++++++++-------- extensions/ffmpeg/README.md | 30 ++++++----------------------- extensions/flac/README.md | 30 ++++++----------------------- extensions/gvr/README.md | 14 ++++++++++---- extensions/ima/README.md | 9 +++++++++ extensions/okhttp/README.md | 22 +++++++++------------ extensions/opus/README.md | 30 ++++++----------------------- extensions/vp9/README.md | 31 ++++++------------------------ 9 files changed, 101 insertions(+), 122 deletions(-) diff --git a/README.md b/README.md index 3de86d21a3..d7bc23f700 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,11 @@ and extend, and can be updated through Play Store application updates. ## Using ExoPlayer ## +ExoPlayer modules can be obtained via jCenter. It's also possible to clone the +repository and depend on the modules locally. + +### Via jCenter ### + The easiest way to get started using ExoPlayer is to add it as a gradle dependency. You need to make sure you have the jcenter repository included in the `build.gradle` file in the root of your project: @@ -64,6 +69,39 @@ latest versions, see the [Release notes][]. [Bintray]: https://bintray.com/google/exoplayer [Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md +### Locally ### + +Cloning the repository and depending on the modules locally is required when +using some ExoPlayer extension modules. It's also a suitable approach if you +want to make local changes to ExoPlayer, or if you want to use a development +branch. + +First, clone the repository into a local directory and checkout the desired +branch: + +```sh +git clone https://github.com/google/ExoPlayer.git +git checkout release-v2 +``` + +Next, add the following to your project's `settings.gradle` file, replacing +`path/to/exoplayer` with the path to your local copy: + +```gradle +gradle.ext.exoplayerRoot = 'path/to/exoplayer' +gradle.ext.exoplayerModulePrefix = 'exoplayer-' +apply from: new File(gradle.ext.exoplayerRoot, 'core_settings.gradle') +``` + +You should now see the ExoPlayer modules appear as part of your project. You can +depend on them as you would on any other local module, for example: + +```gradle +compile project(':exoplayer-library-core') +compile project(':exoplayer-library-dash') +compile project(':exoplayer-library-ui) +``` + ## Developing ExoPlayer ## #### Project branches #### diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md index a570385a52..30409fa99e 100644 --- a/extensions/cronet/README.md +++ b/extensions/cronet/README.md @@ -11,13 +11,10 @@ The Cronet Extension is an [HttpDataSource][] implementation using [Cronet][]. ## Build Instructions ## -* Checkout ExoPlayer along with Extensions: - -``` -git clone https://github.com/google/ExoPlayer.git -``` - -* Get the Cronet libraries: +To use this extension you need to clone the ExoPlayer repository and depend on +its modules locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. In addition, it's necessary to get the Cronet libraries +and enable the extension: 1. Find the latest Cronet release [here][] and navigate to its `Release/cronet` directory @@ -27,6 +24,12 @@ git clone https://github.com/google/ExoPlayer.git 1. Copy the content of the downloaded `libs` directory into the `jniLibs` directory of this extension -* In ExoPlayer's `settings.gradle` file, uncomment the Cronet extension +* In your `settings.gradle` file, add the following line before the line that + applies `core_settings.gradle`: +```gradle +gradle.ext.exoplayerIncludeCronetExtension = true; +``` + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index 4ce9173ec9..ab3e5ffb94 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -9,11 +9,10 @@ audio. ## Build instructions ## -* Checkout ExoPlayer along with Extensions - -``` -git clone https://github.com/google/ExoPlayer.git -``` +To use this extension you need to clone the ExoPlayer repository and depend on +its modules locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. In addition, it's necessary to build the extension's +native components as follows: * Set the following environment variables: @@ -25,8 +24,6 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main" * Download the [Android NDK][] and set its location in an environment variable: -[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html - ``` NDK_PATH="" ``` @@ -106,20 +103,5 @@ cd "${FFMPEG_EXT_PATH}"/jni && \ ${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4 ``` -* In your project, you can add a dependency on the extension by using a rule - like this: - -``` -// in settings.gradle -include ':..:ExoPlayer:library' -include ':..:ExoPlayer:extension-ffmpeg' - -// in build.gradle -dependencies { - compile project(':..:ExoPlayer:library') - compile project(':..:ExoPlayer:extension-ffmpeg') -} -``` - -* Now, when you build your app, the extension will be built and the native - libraries will be packaged along with the APK. +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md +[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html diff --git a/extensions/flac/README.md b/extensions/flac/README.md index 2f3b067d6f..a35dac7858 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -10,11 +10,10 @@ ExoPlayer to play Flac audio on Android devices. ## Build Instructions ## -* Checkout ExoPlayer along with Extensions: - -``` -git clone https://github.com/google/ExoPlayer.git -``` +To use this extension you need to clone the ExoPlayer repository and depend on +its modules locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. In addition, it's necessary to build the extension's +native components as follows: * Set the following environment variables: @@ -26,8 +25,6 @@ FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main" * Download the [Android NDK][] and set its location in an environment variable: -[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html - ``` NDK_PATH="" ``` @@ -47,20 +44,5 @@ cd "${FLAC_EXT_PATH}"/jni && \ ${NDK_PATH}/ndk-build APP_ABI=all -j4 ``` -* In your project, you can add a dependency to the Flac Extension by using a - rule like this: - -``` -// in settings.gradle -include ':..:ExoPlayer:library' -include ':..:ExoPlayer:extension-flac' - -// in build.gradle -dependencies { - compile project(':..:ExoPlayer:library') - compile project(':..:ExoPlayer:extension-flac') -} -``` - -* Now, when you build your app, the Flac extension will be built and the native - libraries will be packaged along with the APK. +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md +[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md index bae5de4812..ad28569121 100644 --- a/extensions/gvr/README.md +++ b/extensions/gvr/README.md @@ -6,7 +6,10 @@ The GVR extension wraps the [Google VR SDK for Android][]. It provides a GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering of surround sound and ambisonic soundfields. -## Using the extension ## +[Google VR SDK for Android]: https://developers.google.com/vr/android/ +[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround + +## Getting the extension ## The easiest way to use the extension is to add it as a gradle dependency. You need to make sure you have the jcenter repository included in the `build.gradle` @@ -27,12 +30,15 @@ compile 'com.google.android.exoplayer:extension-gvr:rX.X.X' where `rX.X.X` is the version, which must match the version of the ExoPlayer library being used. -## Using GvrAudioProcessor ## +Alternatively, you can clone the ExoPlayer repository and depend on the module +locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. + +## Using the extension ## * If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to return a GvrAudioProcessor. * If constructing renderers directly, pass a GvrAudioProcessor to MediaCodecAudioRenderer's constructor. -[Google VR SDK for Android]: https://developers.google.com/vr/android/ -[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md diff --git a/extensions/ima/README.md b/extensions/ima/README.md index aabe84136f..aaae44edcf 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -9,6 +9,14 @@ alongside content. [IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/ [MediaSource]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +## Getting the extension ## + +To use this extension you need to clone the ExoPlayer repository and depend on +its modules 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 + ## Using the extension ## Pass a single-window content `MediaSource` to `ImaAdsMediaSource`'s constructor, @@ -21,6 +29,7 @@ select and build one of the `withExtensions` build variants of the demo app in Android Studio. You can find IMA test content in the "IMA sample ad tags" section of the app. +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags ## Known issues ## diff --git a/extensions/okhttp/README.md b/extensions/okhttp/README.md index d84dcb44ec..52d5fabf38 100644 --- a/extensions/okhttp/README.md +++ b/extensions/okhttp/README.md @@ -5,19 +5,12 @@ The OkHttp Extension is an [HttpDataSource][] implementation using Square's [OkHttp][]. -## Using the extension ## +[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html +[OkHttp]: https://square.github.io/okhttp/ -The easiest way to use the extension is to add it as a gradle dependency. You -need to make sure you have the jcenter repository included in the `build.gradle` -file in the root of your project: +## Getting the extension ## -```gradle -repositories { - jcenter() -} -``` - -Next, include the following in your module's `build.gradle` file: +The easiest way to use the extension is to add it as a gradle dependency: ```gradle compile 'com.google.android.exoplayer:extension-okhttp:rX.X.X' @@ -26,5 +19,8 @@ compile 'com.google.android.exoplayer:extension-okhttp:rX.X.X' where `rX.X.X` is the version, which must match the version of the ExoPlayer library being used. -[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html -[OkHttp]: https://square.github.io/okhttp/ +Alternatively, you can clone the ExoPlayer repository and depend on the module +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 diff --git a/extensions/opus/README.md b/extensions/opus/README.md index 36ca2b7261..ae42a9c310 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -10,11 +10,10 @@ ExoPlayer to play Opus audio on Android devices. ## Build Instructions ## -* Checkout ExoPlayer along with Extensions: - -``` -git clone https://github.com/google/ExoPlayer.git -``` +To use this extension you need to clone the ExoPlayer repository and depend on +its modules locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. In addition, it's necessary to build the extension's +native components as follows: * Set the following environment variables: @@ -26,8 +25,6 @@ OPUS_EXT_PATH="${EXOPLAYER_ROOT}/extensions/opus/src/main" * Download the [Android NDK][] and set its location in an environment variable: -[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html - ``` NDK_PATH="" ``` @@ -52,23 +49,8 @@ cd "${OPUS_EXT_PATH}"/jni && \ ${NDK_PATH}/ndk-build APP_ABI=all -j4 ``` -* In your project, you can add a dependency to the Opus Extension by using a -rule like this: - -``` -// in settings.gradle -include ':..:ExoPlayer:library' -include ':..:ExoPlayer:extension-opus' - -// in build.gradle -dependencies { - compile project(':..:ExoPlayer:library') - compile project(':..:ExoPlayer:extension-opus') -} -``` - -* Now, when you build your app, the Opus extension will be built and the native - libraries will be packaged along with the APK. +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md +[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html ## Notes ## diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 53ef4b0bfd..8bdfe652e6 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -10,11 +10,10 @@ VP9 video on Android devices. ## Build Instructions ## -* Checkout ExoPlayer along with Extensions: - -``` -git clone https://github.com/google/ExoPlayer.git -``` +To use this extension you need to clone the ExoPlayer repository and depend on +its modules locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. In addition, it's necessary to build the extension's +native components as follows: * Set the following environment variables: @@ -26,8 +25,6 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" * Download the [Android NDK][] and set its location in an environment variable: -[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html - ``` NDK_PATH="" ``` @@ -66,23 +63,8 @@ cd "${VP9_EXT_PATH}"/jni && \ ${NDK_PATH}/ndk-build APP_ABI=all -j4 ``` -* In your project, you can add a dependency to the VP9 Extension by using a the - following rule: - -``` -// in settings.gradle -include ':..:ExoPlayer:library' -include ':..:ExoPlayer:extension-vp9' - -// in build.gradle -dependencies { - compile project(':..:ExoPlayer:library') - compile project(':..:ExoPlayer:extension-vp9') -} -``` - -* Now, when you build your app, the VP9 extension will be built and the native - libraries will be packaged along with the APK. +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md +[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html ## Notes ## @@ -94,4 +76,3 @@ dependencies { `${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But please note that `generate_libvpx_android_configs.sh` and the makefiles need to be modified to work with arbitrary versions of libvpx and libyuv. - From 66d122710ee9df56000f8f35ab709f847dcd748f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 27 Jun 2017 10:40:34 -0700 Subject: [PATCH 182/220] Add support for in-period ads ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160294524 --- .../android/exoplayer2/ExoPlayerImpl.java | 16 +- .../exoplayer2/ExoPlayerImplInternal.java | 253 +++++++------- .../exoplayer2/MediaPeriodInfoSequence.java | 327 ++++++++++++++++++ .../google/android/exoplayer2/Timeline.java | 177 +++++++++- .../exoplayer2/source/MediaSource.java | 69 +++- 5 files changed, 713 insertions(+), 129 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java 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 a540b18974..500dd9a058 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 @@ -24,6 +24,7 @@ import android.util.Log; import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo; import com.google.android.exoplayer2.ExoPlayerImplInternal.SourceInfo; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -299,7 +300,14 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty()) { return C.TIME_UNSET; } - return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); + if (isPlayingAd()) { + MediaPeriodId periodId = playbackInfo.periodId; + timeline.getPeriod(periodId.periodIndex, period); + long adDurationUs = period.getAdDurationUs(periodId.adGroupIndex, periodId.adIndexInAdGroup); + return C.usToMs(adDurationUs); + } else { + return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); + } } @Override @@ -463,4 +471,10 @@ import java.util.concurrent.CopyOnWriteArraySet; } } + // TODO: Add to the public ExoPlayer interface. + + private boolean isPlayingAd() { + return pendingSeekAcks == 0 && playbackInfo.periodId.adGroupIndex != C.INDEX_UNSET; + } + } 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 1331b8b0cd..8e28039735 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 @@ -24,6 +24,8 @@ import android.os.SystemClock; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; +import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; +import com.google.android.exoplayer2.source.ClippingMediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; @@ -150,6 +152,7 @@ import java.io.IOException; private final ExoPlayer player; private final Timeline.Window window; private final Timeline.Period period; + private final MediaPeriodInfoSequence mediaPeriodInfoSequence; private PlaybackInfo playbackInfo; private PlaybackParameters playbackParameters; @@ -199,6 +202,7 @@ import java.io.IOException; enabledRenderers = new Renderer[0]; window = new Timeline.Window(); period = new Timeline.Period(); + mediaPeriodInfoSequence = new MediaPeriodInfoSequence(); trackSelector.init(this); playbackParameters = PlaybackParameters.DEFAULT; @@ -435,6 +439,7 @@ import java.io.IOException; private void setRepeatModeInternal(@ExoPlayer.RepeatMode int repeatMode) throws ExoPlaybackException { this.repeatMode = repeatMode; + mediaPeriodInfoSequence.setRepeatMode(repeatMode); // Find the last existing period holder that matches the new period order. MediaPeriodHolder lastValidPeriodHolder = playingPeriodHolder != null @@ -442,13 +447,18 @@ import java.io.IOException; if (lastValidPeriodHolder == null) { return; } - int nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.periodIndex, period, - window, repeatMode); - while (lastValidPeriodHolder.next != null && nextPeriodIndex != C.INDEX_UNSET - && lastValidPeriodHolder.next.periodIndex == nextPeriodIndex) { + while (true) { + int nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.info.id.periodIndex, + period, window, repeatMode); + while (lastValidPeriodHolder.next != null + && !lastValidPeriodHolder.info.isLastInTimelinePeriod) { + lastValidPeriodHolder = lastValidPeriodHolder.next; + } + if (nextPeriodIndex == C.INDEX_UNSET || lastValidPeriodHolder.next == null + || lastValidPeriodHolder.next.info.id.periodIndex != nextPeriodIndex) { + break; + } lastValidPeriodHolder = lastValidPeriodHolder.next; - nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.periodIndex, period, - window, repeatMode); } // Release any period holders that don't match the new period order. @@ -459,7 +469,10 @@ import java.io.IOException; releasePeriodHoldersFrom(lastValidPeriodHolder.next); lastValidPeriodHolder.next = null; } - lastValidPeriodHolder.isFinal = isFinalPeriod(lastValidPeriodHolder.periodIndex); + + // Update the period info for the last holder, as it may now be the last period in the timeline. + lastValidPeriodHolder.info = + mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info); // Handle cases where loadingPeriodHolder or readingPeriodHolder have been removed. boolean seenLoadingPeriodHolder = loadingPeriodHolderIndex <= lastValidPeriodHolder.index; @@ -471,9 +484,9 @@ import java.io.IOException; if (!seenReadingPeriodHolder && playingPeriodHolder != null) { // Renderers may have read from a period that's been removed. Seek back to the current // position of the playing period to make sure none of the removed period is played. - int playingPeriodIndex = playingPeriodHolder.periodIndex; - long newPositionUs = seekToPeriodPosition(playingPeriodIndex, playbackInfo.positionUs); - playbackInfo = new PlaybackInfo(playingPeriodIndex, newPositionUs); + MediaPeriodId periodId = playingPeriodHolder.info.id; + long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.positionUs); + playbackInfo = new PlaybackInfo(periodId, newPositionUs); } // Restart buffering if playback has ended and repetition is enabled. @@ -522,8 +535,7 @@ import java.io.IOException; long bufferedPositionUs = enabledRenderers.length == 0 ? C.TIME_END_OF_SOURCE : playingPeriodHolder.mediaPeriod.getBufferedPositionUs(); playbackInfo.bufferedPositionUs = bufferedPositionUs == C.TIME_END_OF_SOURCE - ? timeline.getPeriod(playingPeriodHolder.periodIndex, period).getDurationUs() - : bufferedPositionUs; + ? playingPeriodHolder.info.durationUs : bufferedPositionUs; } private void doSomeWork() throws ExoPlaybackException, IOException { @@ -575,12 +587,11 @@ import java.io.IOException; } } - long playingPeriodDurationUs = timeline.getPeriod(playingPeriodHolder.periodIndex, period) - .getDurationUs(); + long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; if (allRenderersEnded && (playingPeriodDurationUs == C.TIME_UNSET || playingPeriodDurationUs <= playbackInfo.positionUs) - && playingPeriodHolder.isFinal) { + && playingPeriodHolder.info.isFinal) { setState(ExoPlayer.STATE_ENDED); stopRenderers(); } else if (state == ExoPlayer.STATE_BUFFERING) { @@ -656,24 +667,30 @@ import java.io.IOException; boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; int periodIndex = periodPosition.first; long periodPositionUs = periodPosition.second; - + MediaPeriodId periodId = + mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, periodPositionUs); + if (periodId.isAd()) { + seekPositionAdjusted = true; + // TODO: Resume content at periodPositionUs after the ad plays. + periodPositionUs = 0; + } try { - if (periodIndex == playbackInfo.periodId.periodIndex + if (periodId.equals(playbackInfo.periodId) && ((periodPositionUs / 1000) == (playbackInfo.positionUs / 1000))) { // Seek position equals the current position. Do nothing. return; } - long newPeriodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs); + long newPeriodPositionUs = seekToPeriodPosition(periodId, periodPositionUs); seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; periodPositionUs = newPeriodPositionUs; } finally { - playbackInfo = new PlaybackInfo(periodIndex, periodPositionUs); + playbackInfo = new PlaybackInfo(periodId, periodPositionUs); eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo) .sendToTarget(); } } - private long seekToPeriodPosition(int periodIndex, long periodPositionUs) + private long seekToPeriodPosition(MediaPeriodId periodId, long periodPositionUs) throws ExoPlaybackException { stopRenderers(); rebuffering = false; @@ -689,7 +706,7 @@ import java.io.IOException; // Clear the timeline, but keep the requested period if it is already prepared. MediaPeriodHolder periodHolder = playingPeriodHolder; while (periodHolder != null) { - if (periodHolder.periodIndex == periodIndex && periodHolder.prepared) { + if (shouldKeepPeriodHolder(periodId, periodPositionUs, periodHolder)) { newPlayingPeriodHolder = periodHolder; } else { periodHolder.release(); @@ -733,6 +750,19 @@ import java.io.IOException; return periodPositionUs; } + private boolean shouldKeepPeriodHolder(MediaPeriodId seekPeriodId, long positionUs, + MediaPeriodHolder holder) { + if (seekPeriodId.equals(holder.info.id) && holder.prepared) { + timeline.getPeriod(holder.info.id.periodIndex, period); + int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs); + if (nextAdGroupIndex == C.INDEX_UNSET + || period.getAdGroupTimeUs(nextAdGroupIndex) == holder.info.endPositionUs) { + return true; + } + } + return false; + } + private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException { rendererPositionUs = playingPeriodHolder == null ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US @@ -795,6 +825,7 @@ import java.io.IOException; mediaSource.releaseSource(); mediaSource = null; } + mediaPeriodInfoSequence.setTimeline(null); timeline = null; } } @@ -905,7 +936,7 @@ import java.io.IOException; } loadingPeriodHolder.next = null; if (loadingPeriodHolder.prepared) { - long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.startPositionUs, + long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.info.startPositionUs, loadingPeriodHolder.toPeriodTime(rendererPositionUs)); loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false); } @@ -918,19 +949,19 @@ import java.io.IOException; private boolean isTimelineReady(long playingPeriodDurationUs) { return playingPeriodDurationUs == C.TIME_UNSET || playbackInfo.positionUs < playingPeriodDurationUs - || (playingPeriodHolder.next != null && playingPeriodHolder.next.prepared); + || (playingPeriodHolder.next != null + && (playingPeriodHolder.next.prepared || playingPeriodHolder.next.info.id.isAd())); } private boolean haveSufficientBuffer(boolean rebuffering) { long loadingPeriodBufferedPositionUs = !loadingPeriodHolder.prepared - ? loadingPeriodHolder.startPositionUs + ? loadingPeriodHolder.info.startPositionUs : loadingPeriodHolder.mediaPeriod.getBufferedPositionUs(); if (loadingPeriodBufferedPositionUs == C.TIME_END_OF_SOURCE) { - if (loadingPeriodHolder.isFinal) { + if (loadingPeriodHolder.info.isFinal) { return true; } - loadingPeriodBufferedPositionUs = timeline.getPeriod(loadingPeriodHolder.periodIndex, period) - .getDurationUs(); + loadingPeriodBufferedPositionUs = loadingPeriodHolder.info.durationUs; } return loadControl.shouldStartPlayback( loadingPeriodBufferedPositionUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs), @@ -953,6 +984,7 @@ import java.io.IOException; throws ExoPlaybackException { Timeline oldTimeline = timeline; timeline = timelineAndManifest.first; + mediaPeriodInfoSequence.setTimeline(timeline); Object manifest = timelineAndManifest.second; int processedInitialSeekCount = 0; @@ -968,14 +1000,20 @@ import java.io.IOException; handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); return; } - playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second); + int periodIndex = periodPosition.first; + long positionUs = periodPosition.second; + MediaPeriodId periodId = + mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs); + playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : positionUs); } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); return; } Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); - playbackInfo = new PlaybackInfo(defaultPosition.first, defaultPosition.second); + MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds( + defaultPosition.first, defaultPosition.second); + playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : defaultPosition.second); } } @@ -991,7 +1029,8 @@ import java.io.IOException; if (periodIndex == C.INDEX_UNSET) { // We didn't find the current period in the new timeline. Attempt to resolve a subsequent // period whose window we can restart from. - int newPeriodIndex = resolveSubsequentPeriod(periodHolder.periodIndex, oldTimeline, timeline); + int newPeriodIndex = resolveSubsequentPeriod(periodHolder.info.id.periodIndex, oldTimeline, + timeline); if (newPeriodIndex == C.INDEX_UNSET) { // We failed to resolve a suitable restart position. handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); @@ -1006,23 +1045,29 @@ import java.io.IOException; // Clear the index of each holder that doesn't contain the default position. If a holder // contains the default position then update its index so it can be re-used when seeking. Object newPeriodUid = period.uid; - periodHolder.periodIndex = C.INDEX_UNSET; + periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET); while (periodHolder.next != null) { periodHolder = periodHolder.next; - periodHolder.periodIndex = periodHolder.uid.equals(newPeriodUid) - ? newPeriodIndex : C.INDEX_UNSET; + if (periodHolder.uid.equals(newPeriodUid)) { + periodHolder.info = mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(periodHolder.info, + newPeriodIndex); + } else { + periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET); + } } // Actually do the seek. - newPositionUs = seekToPeriodPosition(newPeriodIndex, newPositionUs); - playbackInfo = new PlaybackInfo(newPeriodIndex, newPositionUs); + MediaPeriodId periodId = new MediaPeriodId(newPeriodIndex); + newPositionUs = seekToPeriodPosition(periodId, newPositionUs); + playbackInfo = new PlaybackInfo(periodId, newPositionUs); notifySourceInfoRefresh(manifest, processedInitialSeekCount); return; } // The current period is in the new timeline. Update the holder and playbackInfo. - periodHolder.setPeriodIndex(periodIndex, isFinalPeriod(periodIndex)); + periodHolder = updatePeriodInfo(periodHolder, periodIndex); if (periodIndex != playbackInfo.periodId.periodIndex) { - playbackInfo = playbackInfo.copyWithPeriodId(new MediaPeriodId(periodIndex)); + playbackInfo = + playbackInfo.copyWithPeriodId(playbackInfo.periodId.copyWithPeriodIndex(periodIndex)); } // If there are subsequent holders, update the index for each of them. If we find a holder @@ -1034,7 +1079,7 @@ import java.io.IOException; if (periodIndex != C.INDEX_UNSET && periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) { // The holder is consistent with the new timeline. Update its index and continue. - periodHolder.setPeriodIndex(periodIndex, isFinalPeriod(periodIndex)); + periodHolder = updatePeriodInfo(periodHolder, periodIndex); } else { // The holder is inconsistent with the new timeline. boolean seenReadingPeriodHolder = @@ -1042,9 +1087,9 @@ import java.io.IOException; if (!seenReadingPeriodHolder) { // Renderers may have read from a period that's been removed. Seek back to the current // position of the playing period to make sure none of the removed period is played. - periodIndex = playingPeriodHolder.periodIndex; - long newPositionUs = seekToPeriodPosition(periodIndex, playbackInfo.positionUs); - playbackInfo = new PlaybackInfo(periodIndex, newPositionUs); + long newPositionUs = + seekToPeriodPosition(playingPeriodHolder.info.id, playbackInfo.positionUs); + playbackInfo = new PlaybackInfo(playingPeriodHolder.info.id, newPositionUs); } else { // Update the loading period to be the last period that's still valid, and release all // subsequent periods. @@ -1060,6 +1105,17 @@ import java.io.IOException; notifySourceInfoRefresh(manifest, processedInitialSeekCount); } + private MediaPeriodHolder updatePeriodInfo(MediaPeriodHolder periodHolder, int periodIndex) { + while (true) { + periodHolder.info = + mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex); + if (periodHolder.info.isLastInTimelinePeriod || periodHolder.next == null) { + return periodHolder; + } + periodHolder = periodHolder.next; + } + } + private void handleSourceInfoRefreshEndedPlayback(Object manifest, int processedInitialSeekCount) { // Set the playback position to (0,0) for notifying the eventHandler. @@ -1103,12 +1159,6 @@ import java.io.IOException; return newPeriodIndex; } - private boolean isFinalPeriod(int periodIndex) { - int windowIndex = timeline.getPeriod(periodIndex, period).windowIndex; - return !timeline.getWindow(windowIndex, window).isDynamic - && timeline.isLastPeriod(periodIndex, period, window, repeatMode); - } - /** * Converts a {@link SeekPosition} into the corresponding (periodIndex, periodPositionUs) for the * internal timeline. @@ -1191,13 +1241,13 @@ import java.io.IOException; // the end of the playing period, so advance playback to the next period. playingPeriodHolder.release(); setPlayingPeriodHolder(playingPeriodHolder.next); - playbackInfo = new PlaybackInfo(playingPeriodHolder.periodIndex, - playingPeriodHolder.startPositionUs); + playbackInfo = new PlaybackInfo(playingPeriodHolder.info.id, + playingPeriodHolder.info.startPositionUs); updatePlaybackPositions(); eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); } - if (readingPeriodHolder.isFinal) { + if (readingPeriodHolder.info.isFinal) { for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; @@ -1261,15 +1311,12 @@ import java.io.IOException; } private void maybeUpdateLoadingPeriod() throws IOException { - int newLoadingPeriodIndex; + MediaPeriodInfo info; if (loadingPeriodHolder == null) { - newLoadingPeriodIndex = playbackInfo.periodId.periodIndex; + info = mediaPeriodInfoSequence.getFirstMediaPeriodInfo(playbackInfo); } else { - int loadingPeriodIndex = loadingPeriodHolder.periodIndex; - if (loadingPeriodHolder.isFinal || !loadingPeriodHolder.isFullyBuffered() - || timeline.getPeriod(loadingPeriodIndex, period).getDurationUs() == C.TIME_UNSET) { - // Either the existing loading period is the last period, or we are not ready to advance to - // loading the next period because it hasn't been fully buffered or its duration is unknown. + if (loadingPeriodHolder.info.isFinal || !loadingPeriodHolder.isFullyBuffered() + || loadingPeriodHolder.info.durationUs == C.TIME_UNSET) { return; } if (playingPeriodHolder != null) { @@ -1279,60 +1326,26 @@ import java.io.IOException; return; } } - newLoadingPeriodIndex = timeline.getNextPeriodIndex(loadingPeriodIndex, period, window, - repeatMode); - if (newLoadingPeriodIndex == C.INDEX_UNSET) { - // The next period is not available yet. - mediaSource.maybeThrowSourceInfoRefreshError(); - return; - } + info = mediaPeriodInfoSequence.getNextMediaPeriodInfo(loadingPeriodHolder.info, + loadingPeriodHolder.getRendererOffset(), rendererPositionUs); } - - long newLoadingPeriodStartPositionUs; - if (loadingPeriodHolder == null) { - newLoadingPeriodStartPositionUs = playbackInfo.positionUs; - } else { - int newLoadingWindowIndex = timeline.getPeriod(newLoadingPeriodIndex, period).windowIndex; - if (newLoadingPeriodIndex - != timeline.getWindow(newLoadingWindowIndex, window).firstPeriodIndex) { - // We're starting to buffer a new period in the current window. Always start from the - // beginning of the period. - newLoadingPeriodStartPositionUs = 0; - } else { - // We're starting to buffer a new window. When playback transitions to this window we'll - // want it to be from its default start position. The expected delay until playback - // transitions is equal the duration of media that's currently buffered (assuming no - // interruptions). Hence we project the default start position forward by the duration of - // the buffer, and start buffering from this point. - long defaultPositionProjectionUs = loadingPeriodHolder.getRendererOffset() - + timeline.getPeriod(loadingPeriodHolder.periodIndex, period).getDurationUs() - - rendererPositionUs; - Pair defaultPosition = timeline.getPeriodPosition(window, period, - newLoadingWindowIndex, C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs)); - if (defaultPosition == null) { - return; - } - - newLoadingPeriodIndex = defaultPosition.first; - newLoadingPeriodStartPositionUs = defaultPosition.second; - } + if (info == null) { + mediaSource.maybeThrowSourceInfoRefreshError(); + return; } long rendererPositionOffsetUs = loadingPeriodHolder == null - ? newLoadingPeriodStartPositionUs + RENDERER_TIMESTAMP_OFFSET_US - : (loadingPeriodHolder.getRendererOffset() - + timeline.getPeriod(loadingPeriodHolder.periodIndex, period).getDurationUs()); + ? (info.startPositionUs + RENDERER_TIMESTAMP_OFFSET_US) + : (loadingPeriodHolder.getRendererOffset() + loadingPeriodHolder.info.durationUs); int holderIndex = loadingPeriodHolder == null ? 0 : loadingPeriodHolder.index + 1; - boolean isLastPeriod = isFinalPeriod(newLoadingPeriodIndex); - timeline.getPeriod(newLoadingPeriodIndex, period, true); + Object uid = timeline.getPeriod(info.id.periodIndex, period, true).uid; MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, - rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, period.uid, - holderIndex, newLoadingPeriodIndex, isLastPeriod, newLoadingPeriodStartPositionUs); + rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, uid, holderIndex, info); if (loadingPeriodHolder != null) { loadingPeriodHolder.next = newPeriodHolder; } loadingPeriodHolder = newPeriodHolder; - loadingPeriodHolder.mediaPeriod.prepare(this, newLoadingPeriodStartPositionUs); + loadingPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); setIsLoading(true); } @@ -1345,7 +1358,7 @@ import java.io.IOException; if (playingPeriodHolder == null) { // This is the first prepared period, so start playing it. readingPeriodHolder = loadingPeriodHolder; - resetRendererPosition(readingPeriodHolder.startPositionUs); + resetRendererPosition(readingPeriodHolder.info.startPositionUs); setPlayingPeriodHolder(readingPeriodHolder); } maybeContinueLoading(); @@ -1473,9 +1486,7 @@ import java.io.IOException; public final boolean[] mayRetainStreamFlags; public final long rendererPositionOffsetUs; - public int periodIndex; - public long startPositionUs; - public boolean isFinal; + public MediaPeriodInfo info; public boolean prepared; public boolean hasEnabledTracks; public MediaPeriodHolder next; @@ -1491,8 +1502,7 @@ import java.io.IOException; public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities, long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl, - MediaSource mediaSource, Object periodUid, int index, int periodIndex, - boolean isFinalPeriod, long startPositionUs) { + MediaSource mediaSource, Object periodUid, int index, MediaPeriodInfo info) { this.renderers = renderers; this.rendererCapabilities = rendererCapabilities; this.rendererPositionOffsetUs = rendererPositionOffsetUs; @@ -1501,13 +1511,16 @@ import java.io.IOException; this.mediaSource = mediaSource; this.uid = Assertions.checkNotNull(periodUid); this.index = index; - this.periodIndex = periodIndex; - this.isFinal = isFinalPeriod; - this.startPositionUs = startPositionUs; + this.info = info; sampleStreams = new SampleStream[renderers.length]; mayRetainStreamFlags = new boolean[renderers.length]; - mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(periodIndex), - loadControl.getAllocator()); + MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, loadControl.getAllocator()); + if (info.endPositionUs != C.TIME_END_OF_SOURCE) { + ClippingMediaPeriod clippingMediaPeriod = new ClippingMediaPeriod(mediaPeriod, true); + clippingMediaPeriod.setClipping(0, info.endPositionUs); + mediaPeriod = clippingMediaPeriod; + } + this.mediaPeriod = mediaPeriod; } public long toRendererTime(long periodTimeUs) { @@ -1519,12 +1532,7 @@ import java.io.IOException; } public long getRendererOffset() { - return rendererPositionOffsetUs - startPositionUs; - } - - public void setPeriodIndex(int periodIndex, boolean isFinal) { - this.periodIndex = periodIndex; - this.isFinal = isFinal; + return rendererPositionOffsetUs - info.startPositionUs; } public boolean isFullyBuffered() { @@ -1535,7 +1543,8 @@ import java.io.IOException; public void handlePrepared() throws ExoPlaybackException { prepared = true; selectTracks(); - startPositionUs = updatePeriodTrackSelection(startPositionUs, false); + long newStartPositionUs = updatePeriodTrackSelection(info.startPositionUs, false); + info = info.copyWithStartPositionUs(newStartPositionUs); } public boolean selectTracks() throws ExoPlaybackException { @@ -1584,7 +1593,11 @@ import java.io.IOException; public void release() { try { - mediaSource.releasePeriod(mediaPeriod); + if (info.endPositionUs != C.TIME_END_OF_SOURCE) { + mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); + } else { + mediaSource.releasePeriod(mediaPeriod); + } } catch (RuntimeException e) { // There's nothing we can do. Log.e(TAG, "Period release failed.", e); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java new file mode 100644 index 0000000000..953736d58b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java @@ -0,0 +1,327 @@ +/* + * 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; + +import android.util.Pair; +import com.google.android.exoplayer2.ExoPlayer.RepeatMode; +import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; + +/** + * Provides a sequence of {@link MediaPeriodInfo}s to the player, determining the order and + * start/end positions for {@link MediaPeriod}s to load and play. + */ +/* package */ final class MediaPeriodInfoSequence { + + // TODO: Consider merging this class with the MediaPeriodHolder queue in ExoPlayerImplInternal. + + /** + * Stores the information required to load and play a {@link MediaPeriod}. + */ + public static final class MediaPeriodInfo { + + /** + * The media period's identifier. + */ + public final MediaPeriodId id; + /** + * The start position of the media to play within the media period, in microseconds. + */ + public final long startPositionUs; + /** + * The end position of the media to play within the media period, in microseconds, or + * {@link C#TIME_END_OF_SOURCE} if the end position is the end of the media period. + */ + public final long endPositionUs; + /** + * The duration of the media to play within the media period, in microseconds, or + * {@link C#TIME_UNSET} if not known. + */ + public final long durationUs; + /** + * Whether this is the last media period in its timeline period (e.g., a postroll ad, or a media + * period corresponding to a timeline period without ads). + */ + public final boolean isLastInTimelinePeriod; + /** + * Whether this is the last media period in the entire timeline. If true, + * {@link #isLastInTimelinePeriod} will also be true. + */ + public final boolean isFinal; + + private MediaPeriodInfo(MediaPeriodId id, long startPositionUs, long endPositionUs, + long durationUs, boolean isLastInTimelinePeriod, boolean isFinal) { + this.id = id; + this.startPositionUs = startPositionUs; + this.endPositionUs = endPositionUs; + this.durationUs = durationUs; + this.isLastInTimelinePeriod = isLastInTimelinePeriod; + this.isFinal = isFinal; + } + + /** + * Returns a copy of this instance with the period identifier's period index set to the + * specified value. + */ + public MediaPeriodInfo copyWithPeriodIndex(int periodIndex) { + return new MediaPeriodInfo(id.copyWithPeriodIndex(periodIndex), startPositionUs, + endPositionUs, durationUs, isLastInTimelinePeriod, isFinal); + } + + /** + * Returns a copy of this instance with the start position set to the specified value. + */ + public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) { + return new MediaPeriodInfo(id, startPositionUs, endPositionUs, durationUs, + isLastInTimelinePeriod, isFinal); + } + + } + + private final Timeline.Period period; + private final Timeline.Window window; + + private Timeline timeline; + @RepeatMode + private int repeatMode; + + /** + * Creates a new media period info sequence. + */ + public MediaPeriodInfoSequence() { + period = new Timeline.Period(); + window = new Timeline.Window(); + } + + /** + * Sets the {@link Timeline}. Call {@link #getUpdatedMediaPeriodInfo} to update period information + * taking into account the new timeline. + */ + public void setTimeline(Timeline timeline) { + this.timeline = timeline; + } + + /** + * Sets the {@link RepeatMode}. Call {@link #getUpdatedMediaPeriodInfo} to update period + * information taking into account the new repeat mode. + */ + public void setRepeatMode(@RepeatMode int repeatMode) { + this.repeatMode = repeatMode; + } + + /** + * Returns the first {@link MediaPeriodInfo} to play, based on the specified playback position. + */ + public MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) { + return getMediaPeriodInfo(playbackInfo.periodId, playbackInfo.startPositionUs); + } + + /** + * Returns the {@link MediaPeriodInfo} following {@code currentMediaPeriodInfo}. + * + * @param currentMediaPeriodInfo The current media period info. + * @param rendererOffsetUs The current renderer offset in microseconds. + * @param rendererPositionUs The current renderer position in microseconds. + * @return The following media period info, or {@code null} if it is not yet possible to get the + * next media period info. + */ + public MediaPeriodInfo getNextMediaPeriodInfo(MediaPeriodInfo currentMediaPeriodInfo, + long rendererOffsetUs, long rendererPositionUs) { + // TODO: This method is called repeatedly from ExoPlayerImplInternal.maybeUpdateLoadingPeriod + // but if the timeline is not ready to provide the next period it can't return a non-null value + // until the timeline is updated. Store whether the next timeline period is ready when the + // timeline is updated, to avoid repeatedly checking the same timeline. + if (currentMediaPeriodInfo.isLastInTimelinePeriod) { + int nextPeriodIndex = timeline.getNextPeriodIndex(currentMediaPeriodInfo.id.periodIndex, + period, window, repeatMode); + if (nextPeriodIndex == C.INDEX_UNSET) { + // We can't create a next period yet. + return null; + } + + long startPositionUs; + int nextWindowIndex = timeline.getPeriod(nextPeriodIndex, period).windowIndex; + if (timeline.getWindow(nextWindowIndex, window).firstPeriodIndex == nextPeriodIndex) { + // We're starting to buffer a new window. When playback transitions to this window we'll + // want it to be from its default start position. The expected delay until playback + // transitions is equal the duration of media that's currently buffered (assuming no + // interruptions). Hence we project the default start position forward by the duration of + // the buffer, and start buffering from this point. + long defaultPositionProjectionUs = + rendererOffsetUs + currentMediaPeriodInfo.durationUs - rendererPositionUs; + Pair defaultPosition = timeline.getPeriodPosition(window, period, + nextWindowIndex, C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs)); + if (defaultPosition == null) { + return null; + } + nextPeriodIndex = defaultPosition.first; + startPositionUs = defaultPosition.second; + } else { + startPositionUs = 0; + } + return getMediaPeriodInfo(resolvePeriodPositionForAds(nextPeriodIndex, startPositionUs), + startPositionUs); + } + + MediaPeriodId currentPeriodId = currentMediaPeriodInfo.id; + if (currentPeriodId.isAd()) { + int currentAdGroupIndex = currentPeriodId.adGroupIndex; + timeline.getPeriod(currentPeriodId.periodIndex, period); + int adCountInCurrentAdGroup = period.getAdGroupCount() == C.LENGTH_UNSET ? C.LENGTH_UNSET + : period.getAdCountInAdGroup(currentAdGroupIndex); + if (adCountInCurrentAdGroup == C.LENGTH_UNSET) { + return null; + } + int nextAdIndexInAdGroup = currentPeriodId.adIndexInAdGroup + 1; + if (nextAdIndexInAdGroup < adCountInCurrentAdGroup) { + // Play the next ad in the ad group if it's available. + return !period.isAdAvailable(currentAdGroupIndex, nextAdIndexInAdGroup) ? null + : getMediaPeriodInfoForAd(currentPeriodId.periodIndex, currentAdGroupIndex, + nextAdIndexInAdGroup); + } else { + // Play content from the ad group position. + return getMediaPeriodInfo(new MediaPeriodId(currentPeriodId.periodIndex), + period.getAdGroupTimeUs(currentAdGroupIndex)); + } + } else if (currentMediaPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE) { + // Play the next ad group if it's available. + int nextAdGroupIndex = + period.getAdGroupIndexForPositionUs(currentMediaPeriodInfo.endPositionUs); + return !period.isAdAvailable(nextAdGroupIndex, 0) ? null + : getMediaPeriodInfoForAd(currentPeriodId.periodIndex, nextAdGroupIndex, 0); + } else { + // Check if the postroll ad should be played. + int adGroupCount = period.getAdGroupCount(); + if (adGroupCount == C.LENGTH_UNSET || adGroupCount == 0 + || period.getAdGroupTimeUs(adGroupCount - 1) != C.TIME_END_OF_SOURCE + || period.hasPlayedAdGroup(adGroupCount - 1) + || !period.isAdAvailable(adGroupCount - 1, 0)) { + return null; + } + return getMediaPeriodInfoForAd(currentPeriodId.periodIndex, adGroupCount - 1, 0); + } + } + + /** + * Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be + * played, returning an identifier for an ad group if one needs to be played before the specified + * position, or an identifier for a content media period if not. + */ + public MediaPeriodId resolvePeriodPositionForAds(int periodIndex, long positionUs) { + timeline.getPeriod(periodIndex, period); + int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs); + return adGroupIndex == C.INDEX_UNSET ? new MediaPeriodId(periodIndex) + : new MediaPeriodId(periodIndex, adGroupIndex, 0); + } + + /** + * Returns the {@code mediaPeriodInfo} updated to take into account the current timeline. + */ + public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo mediaPeriodInfo) { + return getUpdatedMediaPeriodInfo(mediaPeriodInfo, mediaPeriodInfo.id); + } + + /** + * Returns the {@code mediaPeriodInfo} updated to take into account the current timeline, + * resetting the identifier of the media period to the specified {@code newPeriodIndex}. + */ + public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo mediaPeriodInfo, + int newPeriodIndex) { + return getUpdatedMediaPeriodInfo(mediaPeriodInfo, + mediaPeriodInfo.id.copyWithPeriodIndex(newPeriodIndex)); + } + + // Internal methods. + + private MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info, MediaPeriodId newId) { + long startPositionUs = info.startPositionUs; + long endPositionUs = info.endPositionUs; + boolean isLastInPeriod = isLastInPeriod(newId, endPositionUs); + boolean isLastInTimeline = isLastInTimeline(newId, isLastInPeriod); + timeline.getPeriod(newId.periodIndex, period); + long durationUs = newId.isAd() + ? period.getAdDurationUs(newId.adGroupIndex, newId.adIndexInAdGroup) + : (endPositionUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endPositionUs); + return new MediaPeriodInfo(newId, startPositionUs, endPositionUs, durationUs, isLastInPeriod, + isLastInTimeline); + } + + private MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId id, long startPositionUs) { + timeline.getPeriod(id.periodIndex, period); + if (id.isAd()) { + if (!period.isAdAvailable(id.adGroupIndex, id.adIndexInAdGroup)) { + return null; + } + return getMediaPeriodInfoForAd(id.periodIndex, id.adGroupIndex, id.adIndexInAdGroup); + } else { + int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); + long endUs = nextAdGroupIndex == C.INDEX_UNSET ? C.TIME_END_OF_SOURCE + : period.getAdGroupTimeUs(nextAdGroupIndex); + return getMediaPeriodInfoForContent(id.periodIndex, startPositionUs, endUs); + } + } + + private MediaPeriodInfo getMediaPeriodInfoForAd(int periodIndex, int adGroupIndex, + int adIndexInAdGroup) { + MediaPeriodId id = new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup); + boolean isLastInPeriod = isLastInPeriod(id, C.TIME_END_OF_SOURCE); + boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); + long durationUs = timeline.getPeriod(id.periodIndex, period) + .getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup); + return new MediaPeriodInfo(id, 0, C.TIME_END_OF_SOURCE, durationUs, isLastInPeriod, + isLastInTimeline); + } + + private MediaPeriodInfo getMediaPeriodInfoForContent(int periodIndex, long startPositionUs, + long endUs) { + MediaPeriodId id = new MediaPeriodId(periodIndex); + boolean isLastInPeriod = isLastInPeriod(id, endUs); + boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); + timeline.getPeriod(id.periodIndex, period); + long durationUs = endUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endUs; + return new MediaPeriodInfo(id, startPositionUs, endUs, durationUs, isLastInPeriod, + isLastInTimeline); + } + + private boolean isLastInPeriod(MediaPeriodId id, long endPositionUs) { + int adGroupCount = timeline.getPeriod(id.periodIndex, period).getAdGroupCount(); + if (adGroupCount == 0) { + return true; + } + if (adGroupCount == C.LENGTH_UNSET) { + return false; + } + int lastAdGroupIndex = adGroupCount - 1; + boolean periodHasPostrollAd = period.getAdGroupTimeUs(lastAdGroupIndex) == C.TIME_END_OF_SOURCE; + if (!id.isAd()) { + return !periodHasPostrollAd && endPositionUs == C.TIME_END_OF_SOURCE; + } else if (periodHasPostrollAd && id.adGroupIndex == lastAdGroupIndex) { + int adCountInLastAdGroup = period.getAdCountInAdGroup(lastAdGroupIndex); + return adCountInLastAdGroup != C.LENGTH_UNSET + && id.adIndexInAdGroup == adCountInLastAdGroup - 1; + } + return false; + } + + private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) { + int windowIndex = timeline.getPeriod(id.periodIndex, period).windowIndex; + return !timeline.getWindow(windowIndex, window).isDynamic + && timeline.isLastPeriod(id.periodIndex, period, window, repeatMode) + && isLastMediaPeriodInPeriod; + } + +} 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 6b1e2aba53..a8f66231c1 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 @@ -235,7 +235,7 @@ public abstract class Timeline { /** * Holds information about a period in a {@link Timeline}. A period defines a single logical piece - * of media, for example a a media file. See {@link Timeline} for more details. The figure below + * of media, for example a media file. See {@link Timeline} for more details. The figure below * shows some of the information defined by a period, as well as how this information relates to a * corresponding {@link Window} in the timeline. *

        @@ -264,24 +264,77 @@ public abstract class Timeline { */ public long durationUs; + // TODO: Remove this flag now that in-period ads are supported. + /** * Whether this period contains an ad. */ public boolean isAd; private long positionInWindowUs; + private long[] adGroupTimesUs; + private boolean[] hasPlayedAdGroup; + private int[] adCounts; + private boolean[][] isAdAvailable; + private long[][] adDurationsUs; /** * Sets the data held by this period. + * + * @param id An identifier for the period. Not necessarily unique. + * @param uid A unique identifier for the period. + * @param windowIndex The index of the window to which this period belongs. + * @param durationUs The duration of this period in microseconds, or {@link C#TIME_UNSET} if + * unknown. + * @param positionInWindowUs The position of the start of this period relative to the start of + * the window to which it belongs, in milliseconds. May be negative if the start of the + * period is not within the window. + * @param isAd Whether this period is an ad. + * @return This period, for convenience. */ public Period set(Object id, Object uid, int windowIndex, long durationUs, long positionInWindowUs, boolean isAd) { + return set(id, uid, windowIndex, durationUs, positionInWindowUs, isAd, null, null, null, null, + null); + } + + /** + * Sets the data held by this period. + * + * @param id An identifier for the period. Not necessarily unique. + * @param uid A unique identifier for the period. + * @param windowIndex The index of the window to which this period belongs. + * @param durationUs The duration of this period in microseconds, or {@link C#TIME_UNSET} if + * unknown. + * @param positionInWindowUs The position of the start of this period relative to the start of + * the window to which it belongs, in milliseconds. May be negative if the start of the + * period is not within the window. + * @param isAd Whether this period is an ad. + * @param adGroupTimesUs The times of ad groups relative to the start of the period, in + * microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that + * the period has a postroll ad. + * @param hasPlayedAdGroup Whether each ad group has been played. + * @param adCounts The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET} + * if the number of ads is not yet known. + * @param isAdAvailable Whether each ad in each ad group is available. + * @param adDurationsUs The duration of each ad in each ad group, in microseconds. An element + * may be {@link C#TIME_UNSET} if the duration is not yet known. + * @return This period, for convenience. + */ + public Period set(Object id, Object uid, int windowIndex, long durationUs, + long positionInWindowUs, boolean isAd, long[] adGroupTimesUs, boolean[] hasPlayedAdGroup, + int[] adCounts, boolean[][] isAdAvailable, long[][] adDurationsUs) { this.id = id; this.uid = uid; this.windowIndex = windowIndex; this.durationUs = durationUs; this.positionInWindowUs = positionInWindowUs; this.isAd = isAd; + this.adGroupTimesUs = adGroupTimesUs; + this.hasPlayedAdGroup = hasPlayedAdGroup; + this.adCounts = adCounts; + this.isAdAvailable = isAdAvailable; + this.adDurationsUs = adDurationsUs; return this; } @@ -317,6 +370,128 @@ public abstract class Timeline { return positionInWindowUs; } + /** + * Returns the number of ad groups in the period. + */ + public int getAdGroupCount() { + return adGroupTimesUs == null ? 0 : adGroupTimesUs.length; + } + + /** + * Returns the time of the ad group at index {@code adGroupIndex} in the period, in + * microseconds. + * + * @param adGroupIndex The ad group index. + * @return The time of the ad group at the index, in microseconds. + */ + public long getAdGroupTimeUs(int adGroupIndex) { + if (adGroupTimesUs == null) { + throw new IndexOutOfBoundsException(); + } + return adGroupTimesUs[adGroupIndex]; + } + + /** + * Returns whether the ad group at index {@code adGroupIndex} has been played. + * + * @param adGroupIndex The ad group index. + * @return Whether the ad group at index {@code adGroupIndex} has been played. + */ + public boolean hasPlayedAdGroup(int adGroupIndex) { + if (hasPlayedAdGroup == null) { + throw new IndexOutOfBoundsException(); + } + return hasPlayedAdGroup[adGroupIndex]; + } + + /** + * Returns the index of the ad group at or before {@code positionUs}, if that ad group is + * unplayed. Returns {@link C#INDEX_UNSET} if the ad group before {@code positionUs} has been + * played, or if there is no such ad group. + * + * @param positionUs The position at or before which to find an ad group, in microseconds. + * @return The index of the ad group, or {@link C#INDEX_UNSET}. + */ + public int getAdGroupIndexForPositionUs(long positionUs) { + if (adGroupTimesUs == null) { + return C.INDEX_UNSET; + } + // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE. + // In practice we expect there to be few ad groups so the search shouldn't be expensive. + int index = adGroupTimesUs.length - 1; + while (index >= 0 && (adGroupTimesUs[index] == C.TIME_END_OF_SOURCE + || adGroupTimesUs[index] > positionUs)) { + index--; + } + return index >= 0 && !hasPlayedAdGroup(index) ? index : C.INDEX_UNSET; + } + + /** + * Returns the index of the next unplayed ad group after {@code positionUs}. Returns + * {@link C#INDEX_UNSET} if there is no such ad group. + * + * @param positionUs The position after which to find an ad group, in microseconds. + * @return The index of the ad group, or {@link C#INDEX_UNSET}. + */ + public int getAdGroupIndexAfterPositionUs(long positionUs) { + if (adGroupTimesUs == null) { + return C.INDEX_UNSET; + } + // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE. + // In practice we expect there to be few ad groups so the search shouldn't be expensive. + int index = 0; + while (index < adGroupTimesUs.length && adGroupTimesUs[index] != C.TIME_END_OF_SOURCE + && (positionUs >= adGroupTimesUs[index] || hasPlayedAdGroup(index))) { + index++; + } + return index < adGroupTimesUs.length ? index : C.INDEX_UNSET; + } + + /** + * Returns the number of ads in the ad group at index {@code adGroupIndex}, or + * {@link C#LENGTH_UNSET} if not yet known. + * + * @param adGroupIndex The ad group index. + * @return The number of ads in the ad group, or {@link C#LENGTH_UNSET} if not yet known. + */ + public int getAdCountInAdGroup(int adGroupIndex) { + if (adCounts == null) { + throw new IndexOutOfBoundsException(); + } + return adCounts[adGroupIndex]; + } + + /** + * Returns whether the URL for the specified ad is known. + * + * @param adGroupIndex The ad group index. + * @param adIndexInAdGroup The ad index in the ad group. + * @return Whether the URL for the specified ad is known. + */ + public boolean isAdAvailable(int adGroupIndex, int adIndexInAdGroup) { + return isAdAvailable != null && adGroupIndex < isAdAvailable.length + && adIndexInAdGroup < isAdAvailable[adGroupIndex].length + && isAdAvailable[adGroupIndex][adIndexInAdGroup]; + } + + /** + * Returns the duration of the ad at index {@code adIndexInAdGroup} in the ad group at + * {@code adGroupIndex}, in microseconds, or {@link C#TIME_UNSET} if not yet known. + * + * @param adGroupIndex The ad group index. + * @param adIndexInAdGroup The ad index in the ad group. + * @return The duration of the ad, or {@link C#TIME_UNSET} if not yet known. + */ + public long getAdDurationUs(int adGroupIndex, int adIndexInAdGroup) { + if (adDurationsUs == null) { + throw new IndexOutOfBoundsException(); + } + if (adIndexInAdGroup >= adDurationsUs[adGroupIndex].length) { + return C.TIME_UNSET; + } + return adDurationsUs[adGroupIndex][adIndexInAdGroup]; + } + } /** 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 77e3b6be65..790620a80c 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 @@ -47,22 +47,28 @@ public interface MediaSource { */ final class MediaPeriodId { + /** + * Value for unset media period identifiers. + */ + public static final MediaPeriodId UNSET = + new MediaPeriodId(C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET); + /** * The timeline period index. */ public final int periodIndex; /** - * If the media period is in an ad break, the index of the ad break in the period. + * If the media period is in an ad group, the index of the ad group in the period. * {@link C#INDEX_UNSET} otherwise. */ - public final int adBreakIndex; + public final int adGroupIndex; /** - * If the media period is in an ad break, the index of the ad in its ad break in the period. + * If the media period is in an ad group, the index of the ad in its ad group in the period. * {@link C#INDEX_UNSET} otherwise. */ - public final int adIndexInAdBreak; + public final int adIndexInAdGroup; /** * Creates a media period identifier for the specified period in the timeline. @@ -70,10 +76,59 @@ public interface MediaSource { * @param periodIndex The timeline period index. */ public MediaPeriodId(int periodIndex) { - // TODO: Allow creation of MediaPeriodIds for ad breaks. + this(periodIndex, C.INDEX_UNSET, C.INDEX_UNSET); + } + + /** + * Creates a media period identifier that identifies an ad within an ad group at the specified + * timeline period. + * + * @param periodIndex The index of the timeline period that contains the ad group. + * @param adGroupIndex The index of the ad group. + * @param adIndexInAdGroup The index of the ad in the ad group. + */ + public MediaPeriodId(int periodIndex, int adGroupIndex, int adIndexInAdGroup) { this.periodIndex = periodIndex; - adBreakIndex = C.INDEX_UNSET; - adIndexInAdBreak = C.INDEX_UNSET; + this.adGroupIndex = adGroupIndex; + this.adIndexInAdGroup = adIndexInAdGroup; + } + + /** + * Returns a copy of this period identifier but with {@code newPeriodIndex} as its period index. + */ + public MediaPeriodId copyWithPeriodIndex(int newPeriodIndex) { + return periodIndex == newPeriodIndex ? this + : new MediaPeriodId(newPeriodIndex, adGroupIndex, adIndexInAdGroup); + } + + /** + * Returns whether this period identifier identifies an ad in an ad group in a period. + */ + public boolean isAd() { + return adGroupIndex != C.INDEX_UNSET; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + MediaPeriodId periodId = (MediaPeriodId) obj; + return periodIndex == periodId.periodIndex && adGroupIndex == periodId.adGroupIndex + && adIndexInAdGroup == periodId.adIndexInAdGroup; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + periodIndex; + result = 31 * result + adGroupIndex; + result = 31 * result + adIndexInAdGroup; + return result; } } From 96fa66028467596e404ff491443329419efe1df3 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 28 Jun 2017 01:43:54 -0700 Subject: [PATCH 183/220] Expose ad playback information on ExoPlayer Also update the time bar to show ad markers using in-period ads and remove support for periods being marked as ads. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160382805 --- .../exoplayer2/demo/PlayerActivity.java | 2 +- .../google/android/exoplayer2/ExoPlayer.java | 17 +++ .../android/exoplayer2/ExoPlayerImpl.java | 21 +++- .../android/exoplayer2/SimpleExoPlayer.java | 15 +++ .../android/exoplayer2/ui/DefaultTimeBar.java | 18 +-- .../exoplayer2/ui/PlaybackControlView.java | 112 +++++++++--------- .../google/android/exoplayer2/ui/TimeBar.java | 2 +- 7 files changed, 115 insertions(+), 72 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index a5e06fa184..0659041c8b 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -327,7 +327,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay mediaDataSourceFactory, this, adTagUri, adOverlayViewGroup); // The demo app has a non-null overlay frame layout. simpleExoPlayerView.getOverlayFrameLayout().addView(adOverlayViewGroup); - // Show a multi-window time bar, which will include ad break position markers. + // Show a multi-window time bar, which will include ad position markers. simpleExoPlayerView.setShowMultiWindowTimeBar(true); } catch (Exception e) { // Throw if the media source class was not found, or there was an error instantiating it. 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 4ef1caf8c7..067cb9fa3a 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 @@ -546,4 +546,21 @@ public interface ExoPlayer { */ boolean isCurrentWindowSeekable(); + /** + * Returns whether the player is currently playing an ad. + */ + boolean isPlayingAd(); + + /** + * If {@link #isPlayingAd()} returns true, returns the index of the ad group in the period + * currently being played. Returns {@link C#INDEX_UNSET} otherwise. + */ + int getCurrentAdGroupIndex(); + + /** + * If {@link #isPlayingAd()} returns true, returns the index of the ad in its ad group. Returns + * {@link C#INDEX_UNSET} otherwise. + */ + int getCurrentAdIndexInAdGroup(); + } 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 500dd9a058..96dd0bd113 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 @@ -352,6 +352,21 @@ import java.util.concurrent.CopyOnWriteArraySet; return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isSeekable; } + @Override + public boolean isPlayingAd() { + return pendingSeekAcks == 0 && playbackInfo.periodId.adGroupIndex != C.INDEX_UNSET; + } + + @Override + public int getCurrentAdGroupIndex() { + return pendingSeekAcks == 0 ? playbackInfo.periodId.adGroupIndex : C.INDEX_UNSET; + } + + @Override + public int getCurrentAdIndexInAdGroup() { + return pendingSeekAcks == 0 ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET; + } + @Override public int getRendererCount() { return renderers.length; @@ -471,10 +486,4 @@ import java.util.concurrent.CopyOnWriteArraySet; } } - // TODO: Add to the public ExoPlayer interface. - - private boolean isPlayingAd() { - return pendingSeekAcks == 0 && playbackInfo.periodId.adGroupIndex != C.INDEX_UNSET; - } - } 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 9bf98ff0dc..97cc7d349f 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 @@ -662,6 +662,21 @@ public class SimpleExoPlayer implements ExoPlayer { return player.isCurrentWindowSeekable(); } + @Override + public boolean isPlayingAd() { + return player.isPlayingAd(); + } + + @Override + public int getCurrentAdGroupIndex() { + return player.getCurrentAdGroupIndex(); + } + + @Override + public int getCurrentAdIndexInAdGroup() { + return player.getCurrentAdIndexInAdGroup(); + } + // Internal methods. private void removeSurfaceCallbacks() { 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 4ede786175..3683196a31 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 @@ -102,8 +102,8 @@ public class DefaultTimeBar extends View implements TimeBar { private long duration; private long position; private long bufferedPosition; - private int adBreakCount; - private long[] adBreakTimesMs; + private int adGroupCount; + private long[] adGroupTimesMs; /** * Creates a new time bar. @@ -241,10 +241,10 @@ public class DefaultTimeBar extends View implements TimeBar { } @Override - public void setAdBreakTimesMs(@Nullable long[] adBreakTimesMs, int adBreakCount) { - Assertions.checkArgument(adBreakCount == 0 || adBreakTimesMs != null); - this.adBreakCount = adBreakCount; - this.adBreakTimesMs = adBreakTimesMs; + public void setAdGroupTimesMs(@Nullable long[] adGroupTimesMs, int adGroupCount) { + Assertions.checkArgument(adGroupCount == 0 || adGroupTimesMs != null); + this.adGroupCount = adGroupCount; + this.adGroupTimesMs = adGroupTimesMs; update(); } @@ -529,10 +529,10 @@ public class DefaultTimeBar extends View implements TimeBar { canvas.drawRect(scrubberBar.left, barTop, scrubberBar.right, barBottom, playedPaint); } int adMarkerOffset = adMarkerWidth / 2; - for (int i = 0; i < adBreakCount; i++) { - long adBreakTimeMs = Util.constrainValue(adBreakTimesMs[i], 0, duration); + for (int i = 0; i < adGroupCount; i++) { + long adGroupTimeMs = Util.constrainValue(adGroupTimesMs[i], 0, duration); int markerPositionOffset = - (int) (progressBar.width() * adBreakTimeMs / duration) - adMarkerOffset; + (int) (progressBar.width() * adGroupTimeMs / duration) - adMarkerOffset; int markerLeft = progressBar.left + Math.min(progressBar.width() - adMarkerWidth, Math.max(0, markerPositionOffset)); canvas.drawRect(markerLeft, barTop, markerLeft + adMarkerWidth, barBottom, adMarkerPaint); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index b06bbf9735..ce047fbbcd 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -312,7 +312,7 @@ public class PlaybackControlView extends FrameLayout { private int showTimeoutMs; private @RepeatToggleModes int repeatToggleModes; private long hideAtMs; - private long[] adBreakTimesMs; + private long[] adGroupTimesMs; private final Runnable updateProgressAction = new Runnable() { @Override @@ -363,7 +363,7 @@ public class PlaybackControlView extends FrameLayout { window = new Timeline.Window(); formatBuilder = new StringBuilder(); formatter = new Formatter(formatBuilder, Locale.getDefault()); - adBreakTimesMs = new long[0]; + adGroupTimesMs = new long[0]; componentListener = new ComponentListener(); controlDispatcher = DEFAULT_CONTROL_DISPATCHER; @@ -649,7 +649,7 @@ public class PlaybackControlView extends FrameLayout { enablePrevious = !timeline.isFirstWindow(windowIndex, player.getRepeatMode()) || isSeekable || !window.isDynamic; enableNext = !timeline.isLastWindow(windowIndex, player.getRepeatMode()) || window.isDynamic; - if (timeline.getPeriod(player.getCurrentPeriodIndex(), period).isAd) { + if (player.isPlayingAd()) { // Always hide player controls during ads. hide(); } @@ -712,47 +712,52 @@ public class PlaybackControlView extends FrameLayout { long positionUs = 0; long bufferedPositionUs = 0; long durationUs = 0; - boolean isInAdBreak = false; - boolean isPlayingAd = false; - int adBreakCount = 0; + int adGroupTimesMsCount = 0; for (int i = 0; i < windowCount; i++) { timeline.getWindow(i, window); for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) { - if (timeline.getPeriod(j, period).isAd) { - isPlayingAd |= j == periodIndex; - if (!isInAdBreak) { - isInAdBreak = true; - if (adBreakCount == adBreakTimesMs.length) { - adBreakTimesMs = Arrays.copyOf(adBreakTimesMs, - adBreakTimesMs.length == 0 ? 1 : adBreakTimesMs.length * 2); - } - adBreakTimesMs[adBreakCount++] = C.usToMs(durationUs); - } - } else { - isInAdBreak = false; - long periodDurationUs = period.getDurationUs(); - Assertions.checkState(periodDurationUs != C.TIME_UNSET); - long periodDurationInWindowUs = periodDurationUs; - if (j == window.firstPeriodIndex) { - periodDurationInWindowUs -= window.positionInFirstPeriodUs; - } - if (i < periodIndex) { - positionUs += periodDurationInWindowUs; - bufferedPositionUs += periodDurationInWindowUs; - } - durationUs += periodDurationInWindowUs; + long periodDurationUs = timeline.getPeriod(j, period).getDurationUs(); + Assertions.checkState(periodDurationUs != C.TIME_UNSET); + long periodDurationInWindowUs = periodDurationUs; + if (j == window.firstPeriodIndex) { + periodDurationInWindowUs -= window.positionInFirstPeriodUs; } + for (int adGroupIndex = 0; adGroupIndex < period.getAdGroupCount(); adGroupIndex++) { + long adGroupTimeUs = period.getAdGroupTimeUs(adGroupIndex); + if (period.hasPlayedAdGroup(adGroupIndex)) { + // Don't show played ad groups. + continue; + } + if (adGroupTimeUs == C.TIME_END_OF_SOURCE) { + adGroupTimeUs = periodDurationUs; + } + if (j == window.firstPeriodIndex) { + adGroupTimeUs -= window.positionInFirstPeriodUs; + } + if (adGroupTimeUs >= 0 && adGroupTimeUs <= window.durationUs) { + if (adGroupTimesMsCount == adGroupTimesMs.length) { + adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, + adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2); + } + adGroupTimesMs[adGroupTimesMsCount++] = C.usToMs(durationUs + adGroupTimeUs); + } + } + if (i < periodIndex) { + positionUs += periodDurationInWindowUs; + bufferedPositionUs += periodDurationInWindowUs; + } + durationUs += periodDurationInWindowUs; } } position = C.usToMs(positionUs); bufferedPosition = C.usToMs(bufferedPositionUs); duration = C.usToMs(durationUs); - if (!isPlayingAd) { + if (!player.isPlayingAd()) { position += player.getCurrentPosition(); bufferedPosition += player.getBufferedPosition(); } if (timeBar != null) { - timeBar.setAdBreakTimesMs(adBreakTimesMs, adBreakCount); + timeBar.setAdGroupTimesMs(adGroupTimesMs, adGroupTimesMsCount); } } else { position = player.getCurrentPosition(); @@ -898,7 +903,7 @@ public class PlaybackControlView extends FrameLayout { } } - private void seekToTimebarPosition(long timebarPositionMs) { + private void seekToTimeBarPosition(long timebarPositionMs) { if (multiWindowTimeBar) { Timeline timeline = player.getCurrentTimeline(); int windowCount = timeline.getWindowCount(); @@ -906,27 +911,25 @@ public class PlaybackControlView extends FrameLayout { for (int i = 0; i < windowCount; i++) { timeline.getWindow(i, window); for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) { - if (!timeline.getPeriod(j, period).isAd) { - long periodDurationMs = period.getDurationMs(); - if (periodDurationMs == C.TIME_UNSET) { - // Should never happen as canShowMultiWindowTimeBar is true. - throw new IllegalStateException(); - } - if (j == window.firstPeriodIndex) { - periodDurationMs -= window.getPositionInFirstPeriodMs(); - } - if (i == windowCount - 1 && j == window.lastPeriodIndex - && remainingMs >= periodDurationMs) { - // Seeking past the end of the last window should seek to the end of the timeline. - seekTo(i, window.getDurationMs()); - return; - } - if (remainingMs < periodDurationMs) { - seekTo(i, period.getPositionInWindowMs() + remainingMs); - return; - } - remainingMs -= periodDurationMs; + long periodDurationMs = timeline.getPeriod(j, period).getDurationMs(); + if (periodDurationMs == C.TIME_UNSET) { + // Should never happen as canShowMultiWindowTimeBar is true. + throw new IllegalStateException(); } + if (j == window.firstPeriodIndex) { + periodDurationMs -= window.getPositionInFirstPeriodMs(); + } + if (i == windowCount - 1 && j == window.lastPeriodIndex + && remainingMs >= periodDurationMs) { + // Seeking past the end of the last window should seek to the end of the timeline. + seekTo(i, window.getDurationMs()); + return; + } + if (remainingMs < periodDurationMs) { + seekTo(i, period.getPositionInWindowMs() + remainingMs); + return; + } + remainingMs -= periodDurationMs; } } } else { @@ -1028,8 +1031,7 @@ public class PlaybackControlView extends FrameLayout { } int periodCount = timeline.getPeriodCount(); for (int i = 0; i < periodCount; i++) { - timeline.getPeriod(i, period); - if (!period.isAd && period.durationUs == C.TIME_UNSET) { + if (timeline.getPeriod(i, period).durationUs == C.TIME_UNSET) { return false; } } @@ -1056,7 +1058,7 @@ public class PlaybackControlView extends FrameLayout { public void onScrubStop(TimeBar timeBar, long position, boolean canceled) { scrubbing = false; if (!canceled && player != null) { - seekToTimebarPosition(position); + seekToTimeBarPosition(position); } hideAfterTimeout(); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java index 2fd5bff5eb..215688083d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java @@ -84,7 +84,7 @@ public interface TimeBar { * ad breaks in milliseconds. May be {@code null} if there are no ad breaks. * @param adBreakCount The number of ad breaks. */ - void setAdBreakTimesMs(@Nullable long[] adBreakTimesMs, int adBreakCount); + void setAdGroupTimesMs(@Nullable long[] adBreakTimesMs, int adBreakCount); /** * Listener for scrubbing events. From c385ece69db39c904dceeb2026e70646c2bec981 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 28 Jun 2017 02:35:30 -0700 Subject: [PATCH 184/220] Move methods into MediaPeriodHolder. Both methods make extensive use of MediaPeriodHolder internals. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160386401 --- .../exoplayer2/ExoPlayerImplInternal.java | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 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 8e28039735..b6c9ef6f5d 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 @@ -596,7 +596,8 @@ import java.io.IOException; stopRenderers(); } else if (state == ExoPlayer.STATE_BUFFERING) { boolean isNewlyReady = enabledRenderers.length > 0 - ? (allRenderersReadyOrEnded && haveSufficientBuffer(rebuffering)) + ? (allRenderersReadyOrEnded + && loadingPeriodHolder.haveSufficientBuffer(rebuffering, rendererPositionUs)) : isTimelineReady(playingPeriodDurationUs); if (isNewlyReady) { setState(ExoPlayer.STATE_READY); @@ -953,21 +954,6 @@ import java.io.IOException; && (playingPeriodHolder.next.prepared || playingPeriodHolder.next.info.id.isAd())); } - private boolean haveSufficientBuffer(boolean rebuffering) { - long loadingPeriodBufferedPositionUs = !loadingPeriodHolder.prepared - ? loadingPeriodHolder.info.startPositionUs - : loadingPeriodHolder.mediaPeriod.getBufferedPositionUs(); - if (loadingPeriodBufferedPositionUs == C.TIME_END_OF_SOURCE) { - if (loadingPeriodHolder.info.isFinal) { - return true; - } - loadingPeriodBufferedPositionUs = loadingPeriodHolder.info.durationUs; - } - return loadControl.shouldStartPlayback( - loadingPeriodBufferedPositionUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs), - rebuffering); - } - private void maybeThrowPeriodPrepareError() throws IOException { if (loadingPeriodHolder != null && !loadingPeriodHolder.prepared && (readingPeriodHolder == null || readingPeriodHolder.next == loadingPeriodHolder)) { @@ -1373,18 +1359,10 @@ import java.io.IOException; } private void maybeContinueLoading() { - long nextLoadPositionUs = !loadingPeriodHolder.prepared ? 0 - : loadingPeriodHolder.mediaPeriod.getNextLoadPositionUs(); - if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { - setIsLoading(false); - } else { - long loadingPeriodPositionUs = loadingPeriodHolder.toPeriodTime(rendererPositionUs); - long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs; - boolean continueLoading = loadControl.shouldContinueLoading(bufferedDurationUs); - setIsLoading(continueLoading); - if (continueLoading) { - loadingPeriodHolder.mediaPeriod.continueLoading(loadingPeriodPositionUs); - } + boolean continueLoading = loadingPeriodHolder.shouldContinueLoading(rendererPositionUs); + setIsLoading(continueLoading); + if (continueLoading) { + loadingPeriodHolder.continueLoading(rendererPositionUs); } } @@ -1540,6 +1518,19 @@ import java.io.IOException; && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE); } + public boolean haveSufficientBuffer(boolean rebuffering, long rendererPositionUs) { + long bufferedPositionUs = !prepared ? info.startPositionUs + : mediaPeriod.getBufferedPositionUs(); + if (bufferedPositionUs == C.TIME_END_OF_SOURCE) { + if (info.isFinal) { + return true; + } + bufferedPositionUs = info.durationUs; + } + return loadControl.shouldStartPlayback(bufferedPositionUs - toPeriodTime(rendererPositionUs), + rebuffering); + } + public void handlePrepared() throws ExoPlaybackException { prepared = true; selectTracks(); @@ -1547,6 +1538,22 @@ import java.io.IOException; info = info.copyWithStartPositionUs(newStartPositionUs); } + public boolean shouldContinueLoading(long rendererPositionUs) { + long nextLoadPositionUs = !prepared ? 0 : mediaPeriod.getNextLoadPositionUs(); + if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { + return false; + } else { + long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs); + long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs; + return loadControl.shouldContinueLoading(bufferedDurationUs); + } + } + + public void continueLoading(long rendererPositionUs) { + long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs); + mediaPeriod.continueLoading(loadingPeriodPositionUs); + } + public boolean selectTracks() throws ExoPlaybackException { TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities, mediaPeriod.getTrackGroups()); From 71ffc7668c552ee2daed2da46424ca3226e104f9 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 28 Jun 2017 08:02:07 -0700 Subject: [PATCH 185/220] Support specifying AudioAttributes for AudioTrack Add a compatibility AudioAttributes class so that the app can specify audio attributes in the same way before and after API 21. Deprecate SimpleExoPlayer.setStreamType. Add SimpleExoPlayer.setAudioAttributes and MSG_SET_AUDIO_ATTRIBUTES. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160408574 --- .../java/com/google/android/exoplayer2/C.java | 186 +++++++++++++++++- .../android/exoplayer2/SimpleExoPlayer.java | 70 +++++-- .../exoplayer2/audio/AudioAttributes.java | 143 ++++++++++++++ .../android/exoplayer2/audio/AudioTrack.java | 144 +++++++------- .../audio/MediaCodecAudioRenderer.java | 6 +- .../audio/SimpleDecoderAudioRenderer.java | 6 +- .../google/android/exoplayer2/util/Util.java | 79 ++++++++ 7 files changed, 529 insertions(+), 105 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java 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 a9eeea6be3..f5bf98716c 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 @@ -189,13 +189,17 @@ public final class C { * Stream types for an {@link android.media.AudioTrack}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({STREAM_TYPE_ALARM, STREAM_TYPE_MUSIC, STREAM_TYPE_NOTIFICATION, STREAM_TYPE_RING, - STREAM_TYPE_SYSTEM, STREAM_TYPE_VOICE_CALL}) + @IntDef({STREAM_TYPE_ALARM, STREAM_TYPE_DTMF, STREAM_TYPE_MUSIC, STREAM_TYPE_NOTIFICATION, + STREAM_TYPE_RING, STREAM_TYPE_SYSTEM, STREAM_TYPE_VOICE_CALL, STREAM_TYPE_USE_DEFAULT}) public @interface StreamType {} /** * @see AudioManager#STREAM_ALARM */ public static final int STREAM_TYPE_ALARM = AudioManager.STREAM_ALARM; + /** + * @see AudioManager#STREAM_DTMF + */ + public static final int STREAM_TYPE_DTMF = AudioManager.STREAM_DTMF; /** * @see AudioManager#STREAM_MUSIC */ @@ -216,11 +220,164 @@ public final class C { * @see AudioManager#STREAM_VOICE_CALL */ public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL; + /** + * @see AudioManager#USE_DEFAULT_STREAM_TYPE + */ + public static final int STREAM_TYPE_USE_DEFAULT = AudioManager.USE_DEFAULT_STREAM_TYPE; /** * The default stream type used by audio renderers. */ public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC; + /** + * Content types for {@link com.google.android.exoplayer2.audio.AudioAttributes}. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({CONTENT_TYPE_MOVIE, CONTENT_TYPE_MUSIC, CONTENT_TYPE_SONIFICATION, CONTENT_TYPE_SPEECH, + CONTENT_TYPE_UNKNOWN}) + public @interface AudioContentType {} + /** + * @see android.media.AudioAttributes#CONTENT_TYPE_MOVIE + */ + @SuppressWarnings("InlinedApi") + public static final int CONTENT_TYPE_MOVIE = android.media.AudioAttributes.CONTENT_TYPE_MOVIE; + /** + * @see android.media.AudioAttributes#CONTENT_TYPE_MUSIC + */ + @SuppressWarnings("InlinedApi") + public static final int CONTENT_TYPE_MUSIC = android.media.AudioAttributes.CONTENT_TYPE_MUSIC; + /** + * @see android.media.AudioAttributes#CONTENT_TYPE_SONIFICATION + */ + @SuppressWarnings("InlinedApi") + public static final int CONTENT_TYPE_SONIFICATION = + android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION; + /** + * @see android.media.AudioAttributes#CONTENT_TYPE_SPEECH + */ + @SuppressWarnings("InlinedApi") + public static final int CONTENT_TYPE_SPEECH = + android.media.AudioAttributes.CONTENT_TYPE_SPEECH; + /** + * @see android.media.AudioAttributes#CONTENT_TYPE_UNKNOWN + */ + @SuppressWarnings("InlinedApi") + public static final int CONTENT_TYPE_UNKNOWN = + android.media.AudioAttributes.CONTENT_TYPE_UNKNOWN; + + /** + * Flags for {@link com.google.android.exoplayer2.audio.AudioAttributes}. + *

        + * Note that {@code FLAG_HW_AV_SYNC} is not available because the player takes care of setting the + * flag when tunneling is enabled via a track selector. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = {FLAG_AUDIBILITY_ENFORCED}) + public @interface AudioFlags {} + /** + * @see android.media.AudioAttributes#FLAG_AUDIBILITY_ENFORCED + */ + @SuppressWarnings("InlinedApi") + public static final int FLAG_AUDIBILITY_ENFORCED = + android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED; + + /** + * Usage types for {@link com.google.android.exoplayer2.audio.AudioAttributes}. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({USAGE_ALARM, USAGE_ASSISTANCE_ACCESSIBILITY, USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, + USAGE_ASSISTANCE_SONIFICATION, USAGE_GAME, USAGE_MEDIA, USAGE_NOTIFICATION, + USAGE_NOTIFICATION_COMMUNICATION_DELAYED, USAGE_NOTIFICATION_COMMUNICATION_INSTANT, + USAGE_NOTIFICATION_COMMUNICATION_REQUEST, USAGE_NOTIFICATION_EVENT, + USAGE_NOTIFICATION_RINGTONE, USAGE_UNKNOWN, USAGE_VOICE_COMMUNICATION, + USAGE_VOICE_COMMUNICATION_SIGNALLING}) + public @interface AudioUsage {} + /** + * @see android.media.AudioAttributes#USAGE_ALARM + */ + @SuppressWarnings("InlinedApi") + public static final int USAGE_ALARM = android.media.AudioAttributes.USAGE_ALARM; + /** + * @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY + */ + @SuppressWarnings("InlinedApi") + public static final int USAGE_ASSISTANCE_ACCESSIBILITY = + android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY; + /** + * @see android.media.AudioAttributes#USAGE_ASSISTANCE_NAVIGATION_GUIDANCE + */ + @SuppressWarnings("InlinedApi") + public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE = + android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE; + /** + * @see android.media.AudioAttributes#USAGE_ASSISTANCE_SONIFICATION + */ + @SuppressWarnings("InlinedApi") + public static final int USAGE_ASSISTANCE_SONIFICATION = + android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION; + /** + * @see android.media.AudioAttributes#USAGE_GAME + */ + @SuppressWarnings("InlinedApi") + public static final int USAGE_GAME = android.media.AudioAttributes.USAGE_GAME; + /** + * @see android.media.AudioAttributes#USAGE_MEDIA + */ + @SuppressWarnings("InlinedApi") + public static final int USAGE_MEDIA = android.media.AudioAttributes.USAGE_MEDIA; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION + */ + @SuppressWarnings("InlinedApi") + public static final int USAGE_NOTIFICATION = android.media.AudioAttributes.USAGE_NOTIFICATION; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_DELAYED + */ + @SuppressWarnings("InlinedApi") + public static final int USAGE_NOTIFICATION_COMMUNICATION_DELAYED = + android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_INSTANT + */ + @SuppressWarnings("InlinedApi") + public static final int USAGE_NOTIFICATION_COMMUNICATION_INSTANT = + android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_REQUEST + */ + @SuppressWarnings("InlinedApi") + public static final int USAGE_NOTIFICATION_COMMUNICATION_REQUEST = + android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION_EVENT + */ + @SuppressWarnings("InlinedApi") + public static final int USAGE_NOTIFICATION_EVENT = + android.media.AudioAttributes.USAGE_NOTIFICATION_EVENT; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION_RINGTONE + */ + @SuppressWarnings("InlinedApi") + public static final int USAGE_NOTIFICATION_RINGTONE = + android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE; + /** + * @see android.media.AudioAttributes#USAGE_UNKNOWN + */ + @SuppressWarnings("InlinedApi") + public static final int USAGE_UNKNOWN = android.media.AudioAttributes.USAGE_UNKNOWN; + /** + * @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION + */ + @SuppressWarnings("InlinedApi") + public static final int USAGE_VOICE_COMMUNICATION = + android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION; + /** + * @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION_SIGNALLING + */ + @SuppressWarnings("InlinedApi") + public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING = + android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING; + /** * Flags which can apply to a buffer containing a media sample. */ @@ -498,16 +655,25 @@ public final class C { /** * A type of a message that can be passed to an audio {@link Renderer} via * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object - * should be one of the integer stream types in {@link C.StreamType}, and will specify the stream - * type of the underlying {@link android.media.AudioTrack}. See also - * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}. If the stream type - * is not set, audio renderers use {@link #STREAM_TYPE_DEFAULT}. + * should be an {@link com.google.android.exoplayer2.audio.AudioAttributes} instance that will + * configure the underlying audio track. If not set, the default audio attributes will be used. + * They are suitable for general media playback. *

        - * Note that when the stream type changes, the AudioTrack must be reinitialized, which can - * introduce a brief gap in audio output. Note also that tracks in the same audio session must - * share the same routing, so a new audio session id will be generated. + * Setting the audio attributes during playback may introduce a short gap in audio output as the + * audio track is recreated. A new audio session id will also be generated. + *

        + * If tunneling is enabled by the track selector, the specified audio attributes will be ignored, + * but they will take effect if audio is later played without tunneling. + *

        + * If the device is running a build before platform API version 21, audio attributes cannot be set + * directly on the underlying audio track. In this case, the usage will be mapped onto an + * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}. + *

        + * To get audio attributes that are equivalent to a legacy stream type, pass the stream type to + * {@link Util#getAudioUsageForStreamType(int)} and use the returned {@link C.AudioUsage} to build + * an audio attributes instance. */ - public static final int MSG_SET_STREAM_TYPE = 3; + public static final int MSG_SET_AUDIO_ATTRIBUTES = 3; /** * The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer} 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 97cc7d349f..054d3e38b9 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 @@ -27,6 +27,7 @@ import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; +import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.metadata.Metadata; @@ -37,6 +38,7 @@ import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.util.List; @@ -105,8 +107,7 @@ public class SimpleExoPlayer implements ExoPlayer { private DecoderCounters videoDecoderCounters; private DecoderCounters audioDecoderCounters; private int audioSessionId; - @C.StreamType - private int audioStreamType; + private AudioAttributes audioAttributes; private float audioVolume; protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, @@ -136,7 +137,7 @@ public class SimpleExoPlayer implements ExoPlayer { // Set initial values. audioVolume = 1; audioSessionId = C.AUDIO_SESSION_ID_UNSET; - audioStreamType = C.STREAM_TYPE_DEFAULT; + audioAttributes = AudioAttributes.DEFAULT; videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; // Build the player and associated objects. @@ -293,33 +294,70 @@ public class SimpleExoPlayer implements ExoPlayer { } /** - * Sets the stream type for audio playback (see {@link C.StreamType} and - * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}). If the stream type - * is not set, audio renderers use {@link C#STREAM_TYPE_DEFAULT}. + * Sets the stream type for audio playback, used by the underlying audio track. *

        - * Note that when the stream type changes, the AudioTrack must be reinitialized, which can - * introduce a brief gap in audio output. Note also that tracks in the same audio session must - * share the same routing, so a new audio session id will be generated. + * Setting the stream type during playback may introduce a short gap in audio output as the audio + * track is recreated. A new audio session id will also be generated. + *

        + * Calling this method overwrites any attributes set previously by calling + * {@link #setAudioAttributes(AudioAttributes)}. * - * @param audioStreamType The stream type for audio playback. + * @deprecated Use {@link #setAudioAttributes(AudioAttributes)}. + * @param streamType The stream type for audio playback. */ - public void setAudioStreamType(@C.StreamType int audioStreamType) { - this.audioStreamType = audioStreamType; + @Deprecated + public void setAudioStreamType(@C.StreamType int streamType) { + @C.AudioUsage int usage = Util.getAudioUsageForStreamType(streamType); + @C.AudioContentType int contentType = Util.getAudioContentTypeForStreamType(streamType); + AudioAttributes audioAttributes = + new AudioAttributes.Builder().setUsage(usage).setContentType(contentType).build(); + setAudioAttributes(audioAttributes); + } + + /** + * Returns the stream type for audio playback. + * + * @deprecated Use {@link #getAudioAttributes()}. + */ + @Deprecated + public @C.StreamType int getAudioStreamType() { + return Util.getStreamTypeForAudioUsage(audioAttributes.usage); + } + + /** + * Sets the attributes for audio playback, used by the underlying audio track. If not set, the + * default audio attributes will be used. They are suitable for general media playback. + *

        + * Setting the audio attributes during playback may introduce a short gap in audio output as the + * audio track is recreated. A new audio session id will also be generated. + *

        + * If tunneling is enabled by the track selector, the specified audio attributes will be ignored, + * but they will take effect if audio is later played without tunneling. + *

        + * If the device is running a build before platform API version 21, audio attributes cannot be set + * directly on the underlying audio track. In this case, the usage will be mapped onto an + * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}. + * + * @param audioAttributes The attributes to use for audio playback. + */ + public void setAudioAttributes(AudioAttributes audioAttributes) { + this.audioAttributes = audioAttributes; ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_STREAM_TYPE, audioStreamType); + messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_AUDIO_ATTRIBUTES, + audioAttributes); } } player.sendMessages(messages); } /** - * Returns the stream type for audio playback. + * Returns the attributes for audio playback. */ - public @C.StreamType int getAudioStreamType() { - return audioStreamType; + public AudioAttributes getAudioAttributes() { + return audioAttributes; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java new file mode 100644 index 0000000000..337200da8f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java @@ -0,0 +1,143 @@ +/* + * 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.audio; + +import android.annotation.TargetApi; +import com.google.android.exoplayer2.C; + +/** + * Attributes for audio playback, which configure the underlying platform + * {@link android.media.AudioTrack}. + *

        + * To set the audio attributes, create an instance using the {@link Builder} and either pass it to + * {@link com.google.android.exoplayer2.SimpleExoPlayer#setAudioAttributes(AudioAttributes)} or + * send a message of type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to the audio renderers. + *

        + * This class is based on {@link android.media.AudioAttributes}, but can be used on all supported + * API versions. + */ +public final class AudioAttributes { + + public static final AudioAttributes DEFAULT = new Builder().build(); + + /** + * Builder for {@link AudioAttributes}. + */ + public static final class Builder { + + @C.AudioContentType + private int contentType; + @C.AudioFlags + private int flags; + @C.AudioUsage + private int usage; + + /** + * Creates a new builder for {@link AudioAttributes}. + *

        + * By default the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage is + * {@link C#USAGE_MEDIA}, and no flags are set. + */ + public Builder() { + contentType = C.CONTENT_TYPE_UNKNOWN; + flags = 0; + usage = C.USAGE_MEDIA; + } + + /** + * @see android.media.AudioAttributes.Builder#setContentType(int) + */ + public Builder setContentType(@C.AudioContentType int contentType) { + this.contentType = contentType; + return this; + } + + /** + * @see android.media.AudioAttributes.Builder#setFlags(int) + */ + public Builder setFlags(@C.AudioFlags int flags) { + this.flags = flags; + return this; + } + + /** + * @see android.media.AudioAttributes.Builder#setUsage(int) + */ + public Builder setUsage(@C.AudioUsage int usage) { + this.usage = usage; + return this; + } + + /** + * Creates an {@link AudioAttributes} instance from this builder. + */ + public AudioAttributes build() { + return new AudioAttributes(contentType, flags, usage); + } + + } + + @C.AudioContentType + public final int contentType; + @C.AudioFlags + public final int flags; + @C.AudioUsage + public final int usage; + + private android.media.AudioAttributes audioAttributesV21; + + private AudioAttributes(@C.AudioContentType int contentType, @C.AudioFlags int flags, + @C.AudioUsage int usage) { + this.contentType = contentType; + this.flags = flags; + this.usage = usage; + } + + @TargetApi(21) + /* package */ android.media.AudioAttributes getAudioAttributesV21() { + if (audioAttributesV21 == null) { + audioAttributesV21 = new android.media.AudioAttributes.Builder() + .setContentType(contentType) + .setFlags(flags) + .setUsage(usage) + .build(); + } + return audioAttributesV21; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + AudioAttributes other = (AudioAttributes) obj; + return this.contentType == other.contentType && this.flags == other.flags + && this.usage == other.usage; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + contentType; + result = 31 * result + flags; + result = 31 * result + usage; + return result; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 92838e34b0..f18e40dec4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.audio; import android.annotation.SuppressLint; import android.annotation.TargetApi; -import android.media.AudioAttributes; import android.media.AudioFormat; +import android.media.AudioManager; import android.media.AudioTimestamp; import android.os.ConditionVariable; import android.os.SystemClock; @@ -40,9 +40,9 @@ import java.util.LinkedList; *

        * Before starting playback, specify the input format by calling * {@link #configure(String, int, int, int, int)}. Optionally call {@link #setAudioSessionId(int)}, - * {@link #setStreamType(int)}, {@link #enableTunnelingV21(int)} and {@link #disableTunneling()} - * to configure audio playback. These methods may be called after writing data to the track, in - * which case it will be reinitialized as required. + * {@link #setAudioAttributes(AudioAttributes)}, {@link #enableTunnelingV21(int)} and + * {@link #disableTunneling()} to configure audio playback. These methods may be called after + * writing data to the track, in which case it will be reinitialized as required. *

        * Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()} * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data. @@ -299,8 +299,7 @@ public final class AudioTrack { private int encoding; @C.Encoding private int outputEncoding; - @C.StreamType - private int streamType; + private AudioAttributes audioAttributes; private boolean passthrough; private int bufferSize; private long bufferSizeUs; @@ -384,7 +383,7 @@ public final class AudioTrack { playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; volume = 1.0f; startMediaTimeState = START_NOT_SET; - streamType = C.STREAM_TYPE_DEFAULT; + audioAttributes = AudioAttributes.DEFAULT; audioSessionId = C.AUDIO_SESSION_ID_UNSET; playbackParameters = PlaybackParameters.DEFAULT; drainingAudioProcessorIndex = C.INDEX_UNSET; @@ -634,19 +633,7 @@ public final class AudioTrack { // initialization of the audio track to fail. releasingConditionVariable.block(); - if (tunneling) { - audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, outputEncoding, - bufferSize, audioSessionId); - } else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { - audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - outputEncoding, bufferSize, MODE_STREAM); - } else { - // Re-attach to the same audio session. - audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - outputEncoding, bufferSize, MODE_STREAM, audioSessionId); - } - checkAudioTrackInitialized(); - + audioTrack = initializeAudioTrack(); int audioSessionId = audioTrack.getAudioSessionId(); if (enablePreV21AudioSessionWorkaround) { if (Util.SDK_INT < 21) { @@ -657,12 +644,7 @@ public final class AudioTrack { releaseKeepSessionIdAudioTrack(); } if (keepSessionIdAudioTrack == null) { - int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE. - int channelConfig = AudioFormat.CHANNEL_OUT_MONO; - @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT; - int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback. - keepSessionIdAudioTrack = new android.media.AudioTrack(streamType, sampleRate, - channelConfig, encoding, bufferSize, MODE_STATIC, audioSessionId); + keepSessionIdAudioTrack = initializeKeepSessionIdAudioTrack(audioSessionId); } } } @@ -1021,23 +1003,23 @@ public final class AudioTrack { } /** - * Sets the stream type for audio track. If the stream type has changed and if the audio track + * Sets the attributes for audio playback. If the attributes have changed and if the audio track * is not configured for use with tunneling, then the audio track is reset and the audio session * id is cleared. *

        - * If the audio track is configured for use with tunneling then the stream type is ignored, the - * audio track is not reset and the audio session id is not cleared. The passed stream type will - * be used if the audio track is later re-configured into non-tunneled mode. + * If the audio track is configured for use with tunneling then the audio attributes are ignored. + * The audio track is not reset and the audio session id is not cleared. The passed attributes + * will be used if the audio track is later re-configured into non-tunneled mode. * - * @param streamType The {@link C.StreamType} to use for audio output. + * @param audioAttributes The attributes for audio playback. */ - public void setStreamType(@C.StreamType int streamType) { - if (this.streamType == streamType) { + public void setAudioAttributes(AudioAttributes audioAttributes) { + if (this.audioAttributes.equals(audioAttributes)) { return; } - this.streamType = streamType; + this.audioAttributes = audioAttributes; if (tunneling) { - // The stream type is ignored in tunneling mode, so no need to reset. + // The audio attributes are ignored in tunneling mode, so no need to reset. return; } reset(); @@ -1333,31 +1315,6 @@ public final class AudioTrack { } } - /** - * Checks that {@link #audioTrack} has been successfully initialized. If it has then calling this - * method is a no-op. If it hasn't then {@link #audioTrack} is released and set to null, and an - * exception is thrown. - * - * @throws InitializationException If {@link #audioTrack} has not been successfully initialized. - */ - private void checkAudioTrackInitialized() throws InitializationException { - int state = audioTrack.getState(); - if (state == STATE_INITIALIZED) { - return; - } - // The track is not successfully initialized. Release and null the track. - try { - audioTrack.release(); - } catch (Exception e) { - // The track has already failed to initialize, so it wouldn't be that surprising if release - // were to fail too. Swallow the exception. - } finally { - audioTrack = null; - } - - throw new InitializationException(state, sampleRate, channelConfig, bufferSize); - } - private boolean isInitialized() { return audioTrack != null; } @@ -1408,24 +1365,65 @@ public final class AudioTrack { && audioTrack.getPlaybackHeadPosition() == 0; } - /** - * Instantiates an {@link android.media.AudioTrack} to be used with tunneling video playback. - */ + private android.media.AudioTrack initializeAudioTrack() throws InitializationException { + android.media.AudioTrack audioTrack; + if (Util.SDK_INT >= 21) { + audioTrack = createAudioTrackV21(); + } else { + int streamType = Util.getStreamTypeForAudioUsage(audioAttributes.usage); + if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { + audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, + outputEncoding, bufferSize, MODE_STREAM); + } else { + // Re-attach to the same audio session. + audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, + outputEncoding, bufferSize, MODE_STREAM, audioSessionId); + } + } + + int state = audioTrack.getState(); + if (state != STATE_INITIALIZED) { + try { + audioTrack.release(); + } catch (Exception e) { + // The track has already failed to initialize, so it wouldn't be that surprising if release + // were to fail too. Swallow the exception. + } + throw new InitializationException(state, sampleRate, channelConfig, bufferSize); + } + return audioTrack; + } + @TargetApi(21) - private static android.media.AudioTrack createHwAvSyncAudioTrackV21(int sampleRate, - int channelConfig, int encoding, int bufferSize, int sessionId) { - AudioAttributes attributesBuilder = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_MEDIA) - .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) - .setFlags(AudioAttributes.FLAG_HW_AV_SYNC) - .build(); + private android.media.AudioTrack createAudioTrackV21() { + android.media.AudioAttributes attributes; + if (tunneling) { + attributes = new android.media.AudioAttributes.Builder() + .setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE) + .setFlags(android.media.AudioAttributes.FLAG_HW_AV_SYNC) + .setUsage(android.media.AudioAttributes.USAGE_MEDIA) + .build(); + } else { + attributes = audioAttributes.getAudioAttributesV21(); + } AudioFormat format = new AudioFormat.Builder() .setChannelMask(channelConfig) - .setEncoding(encoding) + .setEncoding(outputEncoding) .setSampleRate(sampleRate) .build(); - return new android.media.AudioTrack(attributesBuilder, format, bufferSize, MODE_STREAM, - sessionId); + int audioSessionId = this.audioSessionId != C.AUDIO_SESSION_ID_UNSET ? this.audioSessionId + : AudioManager.AUDIO_SESSION_ID_GENERATE; + return new android.media.AudioTrack(attributes, format, bufferSize, MODE_STREAM, + audioSessionId); + } + + private android.media.AudioTrack initializeKeepSessionIdAudioTrack(int audioSessionId) { + int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE. + int channelConfig = AudioFormat.CHANNEL_OUT_MONO; + @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT; + int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback. + return new android.media.AudioTrack(C.STREAM_TYPE_DEFAULT, sampleRate, channelConfig, encoding, + bufferSize, MODE_STATIC, audioSessionId); } @C.Encoding 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 48c7462b03..4d97c292ac 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 @@ -399,9 +399,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media case C.MSG_SET_VOLUME: audioTrack.setVolume((Float) message); break; - case C.MSG_SET_STREAM_TYPE: - @C.StreamType int streamType = (Integer) message; - audioTrack.setStreamType(streamType); + case C.MSG_SET_AUDIO_ATTRIBUTES: + AudioAttributes audioAttributes = (AudioAttributes) message; + audioTrack.setAudioAttributes(audioAttributes); break; default: super.handleMessage(messageType, message); 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 ddb870f6ff..a16a3f3ca2 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 @@ -595,9 +595,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements case C.MSG_SET_VOLUME: audioTrack.setVolume((Float) message); break; - case C.MSG_SET_STREAM_TYPE: - @C.StreamType int streamType = (Integer) message; - audioTrack.setStreamType(streamType); + case C.MSG_SET_AUDIO_ATTRIBUTES: + AudioAttributes audioAttributes = (AudioAttributes) message; + audioTrack.setAudioAttributes(audioAttributes); break; default: super.handleMessage(messageType, message); 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 a65a9cbf44..c00d7fa36c 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 @@ -796,6 +796,85 @@ public final class Util { } } + /** + * Returns the {@link C.AudioUsage} corresponding to the specified {@link C.StreamType}. + */ + @C.AudioUsage + public static int getAudioUsageForStreamType(@C.StreamType int streamType) { + switch (streamType) { + case C.STREAM_TYPE_ALARM: + return C.USAGE_ALARM; + case C.STREAM_TYPE_DTMF: + return C.USAGE_VOICE_COMMUNICATION_SIGNALLING; + case C.STREAM_TYPE_NOTIFICATION: + return C.USAGE_NOTIFICATION; + case C.STREAM_TYPE_RING: + return C.USAGE_NOTIFICATION_RINGTONE; + case C.STREAM_TYPE_SYSTEM: + return C.USAGE_ASSISTANCE_SONIFICATION; + case C.STREAM_TYPE_VOICE_CALL: + return C.USAGE_VOICE_COMMUNICATION; + case C.STREAM_TYPE_USE_DEFAULT: + case C.STREAM_TYPE_MUSIC: + default: + return C.USAGE_MEDIA; + } + } + + /** + * Returns the {@link C.AudioContentType} corresponding to the specified {@link C.StreamType}. + */ + @C.AudioContentType + public static int getAudioContentTypeForStreamType(@C.StreamType int streamType) { + switch (streamType) { + case C.STREAM_TYPE_ALARM: + case C.STREAM_TYPE_DTMF: + case C.STREAM_TYPE_NOTIFICATION: + case C.STREAM_TYPE_RING: + case C.STREAM_TYPE_SYSTEM: + return C.CONTENT_TYPE_SONIFICATION; + case C.STREAM_TYPE_VOICE_CALL: + return C.CONTENT_TYPE_SPEECH; + case C.STREAM_TYPE_USE_DEFAULT: + case C.STREAM_TYPE_MUSIC: + default: + return C.CONTENT_TYPE_MUSIC; + } + } + + /** + * Returns the {@link C.StreamType} corresponding to the specified {@link C.AudioUsage}. + */ + @C.StreamType + public static int getStreamTypeForAudioUsage(@C.AudioUsage int usage) { + switch (usage) { + case C.USAGE_MEDIA: + case C.USAGE_GAME: + case C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: + return C.STREAM_TYPE_MUSIC; + case C.USAGE_ASSISTANCE_SONIFICATION: + return C.STREAM_TYPE_SYSTEM; + case C.USAGE_VOICE_COMMUNICATION: + return C.STREAM_TYPE_VOICE_CALL; + case C.USAGE_VOICE_COMMUNICATION_SIGNALLING: + return C.STREAM_TYPE_DTMF; + case C.USAGE_ALARM: + return C.STREAM_TYPE_ALARM; + case C.USAGE_NOTIFICATION_RINGTONE: + return C.STREAM_TYPE_RING; + case C.USAGE_NOTIFICATION: + case C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST: + case C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT: + case C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED: + case C.USAGE_NOTIFICATION_EVENT: + return C.STREAM_TYPE_NOTIFICATION; + case C.USAGE_ASSISTANCE_ACCESSIBILITY: + case C.USAGE_UNKNOWN: + default: + return C.STREAM_TYPE_DEFAULT; + } + } + /** * Makes a best guess to infer the type from a {@link Uri}. * From c6e5b67626baae294d5c7a99dbac9f384bafb3fe Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 28 Jun 2017 08:42:49 -0700 Subject: [PATCH 186/220] Move InlinedApi warning suppression to class level for C ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160412354 --- .../java/com/google/android/exoplayer2/C.java | 44 +------------------ 1 file changed, 2 insertions(+), 42 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 f5bf98716c..62afbc98a7 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 @@ -31,6 +31,7 @@ import java.util.UUID; /** * Defines constants used by the library. */ +@SuppressWarnings("InlinedApi") public final class C { private C() {} @@ -101,24 +102,20 @@ public final class C { /** * @see MediaCodec#CRYPTO_MODE_UNENCRYPTED */ - @SuppressWarnings("InlinedApi") public static final int CRYPTO_MODE_UNENCRYPTED = MediaCodec.CRYPTO_MODE_UNENCRYPTED; /** * @see MediaCodec#CRYPTO_MODE_AES_CTR */ - @SuppressWarnings("InlinedApi") public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR; /** * @see MediaCodec#CRYPTO_MODE_AES_CBC */ - @SuppressWarnings("InlinedApi") public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC; /** * Represents an unset {@link android.media.AudioTrack} session identifier. Equal to * {@link AudioManager#AUDIO_SESSION_ID_GENERATE}. */ - @SuppressWarnings("InlinedApi") public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE; /** @@ -160,28 +157,24 @@ public final class C { /** * @see AudioFormat#ENCODING_AC3 */ - @SuppressWarnings("InlinedApi") public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; /** * @see AudioFormat#ENCODING_E_AC3 */ - @SuppressWarnings("InlinedApi") public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3; /** * @see AudioFormat#ENCODING_DTS */ - @SuppressWarnings("InlinedApi") public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS; /** * @see AudioFormat#ENCODING_DTS_HD */ - @SuppressWarnings("InlinedApi") public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD; /** * @see AudioFormat#CHANNEL_OUT_7POINT1_SURROUND */ - @SuppressWarnings({"InlinedApi", "deprecation"}) + @SuppressWarnings("deprecation") public static final int CHANNEL_OUT_7POINT1_SURROUND = Util.SDK_INT < 23 ? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; @@ -239,29 +232,24 @@ public final class C { /** * @see android.media.AudioAttributes#CONTENT_TYPE_MOVIE */ - @SuppressWarnings("InlinedApi") public static final int CONTENT_TYPE_MOVIE = android.media.AudioAttributes.CONTENT_TYPE_MOVIE; /** * @see android.media.AudioAttributes#CONTENT_TYPE_MUSIC */ - @SuppressWarnings("InlinedApi") public static final int CONTENT_TYPE_MUSIC = android.media.AudioAttributes.CONTENT_TYPE_MUSIC; /** * @see android.media.AudioAttributes#CONTENT_TYPE_SONIFICATION */ - @SuppressWarnings("InlinedApi") public static final int CONTENT_TYPE_SONIFICATION = android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION; /** * @see android.media.AudioAttributes#CONTENT_TYPE_SPEECH */ - @SuppressWarnings("InlinedApi") public static final int CONTENT_TYPE_SPEECH = android.media.AudioAttributes.CONTENT_TYPE_SPEECH; /** * @see android.media.AudioAttributes#CONTENT_TYPE_UNKNOWN */ - @SuppressWarnings("InlinedApi") public static final int CONTENT_TYPE_UNKNOWN = android.media.AudioAttributes.CONTENT_TYPE_UNKNOWN; @@ -277,7 +265,6 @@ public final class C { /** * @see android.media.AudioAttributes#FLAG_AUDIBILITY_ENFORCED */ - @SuppressWarnings("InlinedApi") public static final int FLAG_AUDIBILITY_ENFORCED = android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED; @@ -295,86 +282,71 @@ public final class C { /** * @see android.media.AudioAttributes#USAGE_ALARM */ - @SuppressWarnings("InlinedApi") public static final int USAGE_ALARM = android.media.AudioAttributes.USAGE_ALARM; /** * @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY */ - @SuppressWarnings("InlinedApi") public static final int USAGE_ASSISTANCE_ACCESSIBILITY = android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY; /** * @see android.media.AudioAttributes#USAGE_ASSISTANCE_NAVIGATION_GUIDANCE */ - @SuppressWarnings("InlinedApi") public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE = android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE; /** * @see android.media.AudioAttributes#USAGE_ASSISTANCE_SONIFICATION */ - @SuppressWarnings("InlinedApi") public static final int USAGE_ASSISTANCE_SONIFICATION = android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION; /** * @see android.media.AudioAttributes#USAGE_GAME */ - @SuppressWarnings("InlinedApi") public static final int USAGE_GAME = android.media.AudioAttributes.USAGE_GAME; /** * @see android.media.AudioAttributes#USAGE_MEDIA */ - @SuppressWarnings("InlinedApi") public static final int USAGE_MEDIA = android.media.AudioAttributes.USAGE_MEDIA; /** * @see android.media.AudioAttributes#USAGE_NOTIFICATION */ - @SuppressWarnings("InlinedApi") public static final int USAGE_NOTIFICATION = android.media.AudioAttributes.USAGE_NOTIFICATION; /** * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_DELAYED */ - @SuppressWarnings("InlinedApi") public static final int USAGE_NOTIFICATION_COMMUNICATION_DELAYED = android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED; /** * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_INSTANT */ - @SuppressWarnings("InlinedApi") public static final int USAGE_NOTIFICATION_COMMUNICATION_INSTANT = android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT; /** * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_REQUEST */ - @SuppressWarnings("InlinedApi") public static final int USAGE_NOTIFICATION_COMMUNICATION_REQUEST = android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST; /** * @see android.media.AudioAttributes#USAGE_NOTIFICATION_EVENT */ - @SuppressWarnings("InlinedApi") public static final int USAGE_NOTIFICATION_EVENT = android.media.AudioAttributes.USAGE_NOTIFICATION_EVENT; /** * @see android.media.AudioAttributes#USAGE_NOTIFICATION_RINGTONE */ - @SuppressWarnings("InlinedApi") public static final int USAGE_NOTIFICATION_RINGTONE = android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE; /** * @see android.media.AudioAttributes#USAGE_UNKNOWN */ - @SuppressWarnings("InlinedApi") public static final int USAGE_UNKNOWN = android.media.AudioAttributes.USAGE_UNKNOWN; /** * @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION */ - @SuppressWarnings("InlinedApi") public static final int USAGE_VOICE_COMMUNICATION = android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION; /** * @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION_SIGNALLING */ - @SuppressWarnings("InlinedApi") public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING = android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING; @@ -388,12 +360,10 @@ public final class C { /** * Indicates that a buffer holds a synchronization sample. */ - @SuppressWarnings("InlinedApi") public static final int BUFFER_FLAG_KEY_FRAME = MediaCodec.BUFFER_FLAG_KEY_FRAME; /** * Flag for empty buffers that signal that the end of the stream was reached. */ - @SuppressWarnings("InlinedApi") public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM; /** * Indicates that a buffer is (at least partially) encrypted. @@ -413,13 +383,11 @@ public final class C { /** * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT */ - @SuppressWarnings("InlinedApi") public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT = MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT; /** * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT */ - @SuppressWarnings("InlinedApi") public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING = MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING; /** @@ -730,17 +698,14 @@ public final class C { /** * @see MediaFormat#COLOR_STANDARD_BT709 */ - @SuppressWarnings("InlinedApi") public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709; /** * @see MediaFormat#COLOR_STANDARD_BT601_PAL */ - @SuppressWarnings("InlinedApi") public static final int COLOR_SPACE_BT601 = MediaFormat.COLOR_STANDARD_BT601_PAL; /** * @see MediaFormat#COLOR_STANDARD_BT2020 */ - @SuppressWarnings("InlinedApi") public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020; /** @@ -752,17 +717,14 @@ public final class C { /** * @see MediaFormat#COLOR_TRANSFER_SDR_VIDEO */ - @SuppressWarnings("InlinedApi") public static final int COLOR_TRANSFER_SDR = MediaFormat.COLOR_TRANSFER_SDR_VIDEO; /** * @see MediaFormat#COLOR_TRANSFER_ST2084 */ - @SuppressWarnings("InlinedApi") public static final int COLOR_TRANSFER_ST2084 = MediaFormat.COLOR_TRANSFER_ST2084; /** * @see MediaFormat#COLOR_TRANSFER_HLG */ - @SuppressWarnings("InlinedApi") public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG; /** @@ -774,12 +736,10 @@ public final class C { /** * @see MediaFormat#COLOR_RANGE_LIMITED */ - @SuppressWarnings("InlinedApi") public static final int COLOR_RANGE_LIMITED = MediaFormat.COLOR_RANGE_LIMITED; /** * @see MediaFormat#COLOR_RANGE_FULL */ - @SuppressWarnings("InlinedApi") public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL; /** From a543436b960cc97ec1dfdb1ac5efe5da6e99fbe0 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 28 Jun 2017 08:57:57 -0700 Subject: [PATCH 187/220] SimpleExoPlayerView/DefaultTimebar fixes - Restore making the playback controls visible on any key press. - Turn anti-aliasing on for drawing the scrubber circle. It looks really ugly on some devices if you don't do this. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160413777 --- .../com/google/android/exoplayer2/demo/PlayerActivity.java | 4 ++-- .../com/google/android/exoplayer2/ui/DefaultTimeBar.java | 5 +++-- .../google/android/exoplayer2/ui/SimpleExoPlayerView.java | 7 ++----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 0659041c8b..b3e9191672 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -205,8 +205,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay @Override public boolean dispatchKeyEvent(KeyEvent event) { - // If the event was not handled then see if the player view can handle it as a media key event. - return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchMediaKeyEvent(event); + // If the event was not handled then see if the player view can handle it. + return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchKeyEvent(event); } // OnClickListener methods 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 3683196a31..cd9b13e4f6 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 @@ -73,10 +73,10 @@ public class DefaultTimeBar extends View implements TimeBar { private final Rect bufferedBar; private final Rect scrubberBar; private final Paint playedPaint; - private final Paint scrubberPaint; private final Paint bufferedPaint; private final Paint unplayedPaint; private final Paint adMarkerPaint; + private final Paint scrubberPaint; private final int barHeight; private final int touchTargetHeight; private final int adMarkerWidth; @@ -115,10 +115,11 @@ public class DefaultTimeBar extends View implements TimeBar { bufferedBar = new Rect(); scrubberBar = new Rect(); playedPaint = new Paint(); - scrubberPaint = new Paint(); bufferedPaint = new Paint(); unplayedPaint = new Paint(); adMarkerPaint = new Paint(); + scrubberPaint = new Paint(); + scrubberPaint.setAntiAlias(true); // Calculate the dimensions and paints for drawn elements. Resources res = context.getResources(); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 3ec9b0943a..fcbb834c18 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -495,6 +495,7 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public boolean dispatchKeyEvent(KeyEvent event) { + maybeShowController(true); return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); } @@ -506,11 +507,7 @@ public final class SimpleExoPlayerView extends FrameLayout { * @return Whether the key event was handled. */ public boolean dispatchMediaKeyEvent(KeyEvent event) { - boolean handled = useController && controller.dispatchMediaKeyEvent(event); - if (handled) { - maybeShowController(true); - } - return handled; + return useController && controller.dispatchMediaKeyEvent(event); } /** From db11e3ddb6f48c897e7e9cdd40ad06b4911ce828 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 29 Jun 2017 00:37:28 -0700 Subject: [PATCH 188/220] Show larger scrubber handle when focused Also remove updateScrubberState as it doesn't do anything useful. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160496133 --- .../android/exoplayer2/ui/DefaultTimeBar.java | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) 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 cd9b13e4f6..cc1e63bec6 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 @@ -89,7 +89,6 @@ public class DefaultTimeBar extends View implements TimeBar { private final Formatter formatter; private final Runnable stopScrubbingRunnable; - private int scrubberSize; private OnScrubListener listener; private int keyCountIncrement; private long keyTimeIncrement; @@ -185,7 +184,6 @@ public class DefaultTimeBar extends View implements TimeBar { stopScrubbing(false); } }; - scrubberSize = scrubberEnabledSize; scrubberPadding = (Math.max(scrubberDisabledSize, Math.max(scrubberEnabledSize, scrubberDraggedSize)) + 1) / 2; @@ -235,8 +233,6 @@ public class DefaultTimeBar extends View implements TimeBar { this.duration = duration; if (scrubbing && duration == C.TIME_UNSET) { stopScrubbing(true); - } else { - updateScrubberState(); } update(); } @@ -252,7 +248,6 @@ public class DefaultTimeBar extends View implements TimeBar { @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); - updateScrubberState(); if (scrubbing && !enabled) { stopScrubbing(true); } @@ -437,7 +432,6 @@ public class DefaultTimeBar extends View implements TimeBar { private void startScrubbing() { scrubbing = true; - updateScrubberState(); ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); @@ -453,18 +447,12 @@ public class DefaultTimeBar extends View implements TimeBar { if (parent != null) { parent.requestDisallowInterceptTouchEvent(false); } - updateScrubberState(); invalidate(); if (listener != null) { listener.onScrubStop(this, getScrubberPosition(), canceled); } } - private void updateScrubberState() { - scrubberSize = scrubbing ? scrubberDraggedSize - : (isEnabled() && duration >= 0 ? scrubberEnabledSize : scrubberDisabledSize); - } - private void update() { bufferedBar.set(progressBar); scrubberBar.set(progressBar); @@ -544,6 +532,8 @@ public class DefaultTimeBar extends View implements TimeBar { if (duration <= 0) { return; } + int scrubberSize = (scrubbing || isFocused()) ? scrubberDraggedSize + : (isEnabled() ? scrubberEnabledSize : scrubberDisabledSize); int playheadRadius = scrubberSize / 2; int playheadCenter = Util.constrainValue(scrubberBar.right, scrubberBar.left, progressBar.right); From 81d077c037e3d559d28fa95e596ef91788587add Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 29 Jun 2017 03:03:25 -0700 Subject: [PATCH 189/220] Extract base class from DashDownloader This base class will be used to extend HlsDownloader from. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160505710 --- .../exoplayer2/source/offline/Downloader.java | 195 ++++++++++++++++++ .../source/offline/DownloaderException.java | 28 +++ 2 files changed, 223 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/offline/Downloader.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/offline/DownloaderException.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/offline/Downloader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/offline/Downloader.java new file mode 100644 index 0000000000..04735551cd --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/offline/Downloader.java @@ -0,0 +1,195 @@ +/* + * 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.source.offline; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataSink; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.FileDataSource; +import com.google.android.exoplayer2.upstream.PriorityDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.upstream.cache.CacheDataSink; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.PriorityTaskManager; +import java.io.IOException; + +/** + * Base class for stream downloaders. + * + *

        All of the methods are blocking. Also they are not thread safe, except {@link + * #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link #getDownloadedBytes()}. + * + * @param The type of the manifest object. + * @param The type of the representation key object. + */ +@ClosedSource(reason = "Not ready yet") +public abstract class Downloader { + + /** + * Listener notified when download progresses. + */ + public interface ProgressListener { + /** + * Called for the first time after the initialization and then after download of each segment. + * It is called on the thread which invoked {@link #downloadRepresentations(ProgressListener)}. + * + * @param downloader The reporting instance. + * @param totalSegments Total number of segments in the content. + * @param downloadedSegments Total number of downloaded segments. + * @param downloadedBytes Total number of downloaded bytes. + * @see #downloadRepresentations(ProgressListener) + */ + void onDownloadProgress(Downloader downloader, int totalSegments, + int downloadedSegments, long downloadedBytes); + } + + protected final Cache cache; + protected final CacheDataSource dataSource; + protected final CacheDataSource offlineDataSource; + protected final PriorityTaskManager priorityTaskManager; + protected final String manifestUri; + + protected volatile int totalSegments; + protected volatile int downloadedSegments; + protected volatile long downloadedBytes; + + /** + * Constructs a Downloader. + * + * @param manifestUri The URI of the manifest to be downloaded. + * @param cache Cache instance to be used to store downloaded data. + * @param upstreamDataSource A {@link DataSource} for downloading data. + * @param cacheReadDataSource A {@link DataSource} for reading data from the cache. + * If null, a {@link FileDataSource} instance is created and used. + * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If + * null, a {@link CacheDataSink} instance is created and used. + * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with + * download. Downloader priority is {@link C#PRIORITY_DOWNLOAD}. + */ + public Downloader(String manifestUri, Cache cache, DataSource upstreamDataSource, + @Nullable DataSource cacheReadDataSource, @Nullable DataSink cacheWriteDataSink, + @Nullable PriorityTaskManager priorityTaskManager) { + if (priorityTaskManager != null) { + upstreamDataSource = + new PriorityDataSource(upstreamDataSource, priorityTaskManager, C.PRIORITY_DOWNLOAD); + } else { + priorityTaskManager = new PriorityTaskManager(); // dummy PriorityTaskManager + } + if (cacheReadDataSource == null) { + cacheReadDataSource = new FileDataSource(); + } + if (cacheWriteDataSink == null) { + cacheWriteDataSink = new CacheDataSink(cache, + CacheDataSource.DEFAULT_MAX_CACHE_FILE_SIZE); + } + + this.manifestUri = manifestUri; + this.cache = cache; + this.dataSource = new CacheDataSource(cache, upstreamDataSource, cacheReadDataSource, + cacheWriteDataSink, CacheDataSource.FLAG_BLOCK_ON_CACHE, null); + this.offlineDataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, + cacheReadDataSource, null, CacheDataSource.FLAG_BLOCK_ON_CACHE, null); + this.priorityTaskManager = priorityTaskManager; + + resetCounters(); + } + + /** + * Downloads the manifest. + * + * @return The downloaded manifest. + * @throws IOException If an error occurs reading data from the stream. + */ + public abstract M downloadManifest() throws IOException; + + /** + * Selects multiple representations pointed to by the keys for downloading, removing or checking + * status. Any previous selection is cleared. + */ + public abstract void selectRepresentations(K... keys); + + /** + * Initializes the total segments, downloaded segments and downloaded bytes counters for the + * selected representations. + * + * @throws IOException Thrown when there is an error while reading from cache. + * @throws DownloaderException Thrown when a representation index is unbounded. + * @throws InterruptedException If the thread has been interrupted. + * @see #getTotalSegments() + * @see #getDownloadedSegments() + * @see #getDownloadedBytes() + */ + public abstract void initStatus() throws DownloaderException, InterruptedException, IOException; + + /** + * Downloads the content for the selected representations in sync or resumes a previously stopped + * download. + * + * @throws IOException Thrown when there is an error while downloading. + * @throws DownloaderException Thrown when no index data can be found for a representation or + * the index is unbounded. + * @throws InterruptedException If the thread has been interrupted. + */ + public abstract void downloadRepresentations(@Nullable ProgressListener listener) + throws IOException, DownloaderException, InterruptedException; + + /** + * Returns the total number of segments in the representations which are selected, or {@link + * C#LENGTH_UNSET} if it hasn't been calculated yet. + * + * @see #initStatus() + */ + public final int getTotalSegments() { + return totalSegments; + } + + /** + * Returns the total number of downloaded segments in the representations which are selected, or + * {@link C#LENGTH_UNSET} if it hasn't been calculated yet. + * + * @see #initStatus() + */ + public final int getDownloadedSegments() { + return downloadedSegments; + } + + /** + * Returns the total number of downloaded bytes in the representations which are selected, or + * {@link C#LENGTH_UNSET} if it hasn't been calculated yet. + * + * @see #initStatus() + */ + public final long getDownloadedBytes() { + return downloadedBytes; + } + + /** + * Removes all representations declared in the manifest and the manifest itself. + * + * @throws InterruptedException Thrown if the thread was interrupted. + */ + public abstract void removeAll() throws InterruptedException; + + protected final void resetCounters() { + totalSegments = C.LENGTH_UNSET; + downloadedSegments = C.LENGTH_UNSET; + downloadedBytes = C.LENGTH_UNSET; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/offline/DownloaderException.java b/library/core/src/main/java/com/google/android/exoplayer2/source/offline/DownloaderException.java new file mode 100644 index 0000000000..c07565b721 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/offline/DownloaderException.java @@ -0,0 +1,28 @@ +/* + * 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.source.offline; + +import com.google.android.exoplayer2.util.ClosedSource; + +/** Thrown on an error in {@link Downloader}. */ +@ClosedSource(reason = "Not ready yet") +public final class DownloaderException extends Exception { + + public DownloaderException(String message) { + super(message); + } + +} From 50530147d01a948ccd277e46bca8b0414bf87254 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 29 Jun 2017 03:28:21 -0700 Subject: [PATCH 190/220] Add a new test and extra checks to DashDownloadServiceTest Modified old testRemoveAction to test removing content after it's fully downloaded. Added a new testRemoveBeforeDownloadComplete which tests removing content before it's fully downloaded. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160507573 --- .../android/exoplayer2/testutil/FakeDataSource.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java index b3f76391e4..57e0ba41dd 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java @@ -63,11 +63,11 @@ import java.util.HashMap; * .newDefaultData() * .appendReadData(defaultData) * .endData() - * .setData("http:///1", data1) + * .setData("http://1", data1) * .newData("test_file") * .appendReadError(new IOException()) - * .appendReadData(data2); - * // No need to call endData at the end + * .appendReadData(data2) + * .endData(); *

        */ public final class FakeDataSource implements DataSource { @@ -139,7 +139,7 @@ public final class FakeDataSource implements DataSource { (int) Math.min(Math.max(0, dataSpec.position - scannedLength), segment.length); scannedLength += segment.length; findingCurrentSegmentIndex &= segment.isErrorSegment() ? segment.exceptionCleared - : segment.bytesRead == segment.length; + : (!segment.isActionSegment() && segment.bytesRead == segment.length); if (findingCurrentSegmentIndex) { currentSegmentIndex++; } From 6509dce6b7cea332b22449bfdd7bade6f74515be Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 29 Jun 2017 04:16:16 -0700 Subject: [PATCH 191/220] Clarify JavaDoc of MediaPeriod. Two of MediaPeriod's methods are only called after the media period has been prepared. Added this to JavaDoc of these method to simplify implementations. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160510373 --- .../com/google/android/exoplayer2/source/MediaPeriod.java | 4 ++++ 1 file changed, 4 insertions(+) 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 90d72dd907..24b7fdc75f 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 @@ -108,6 +108,8 @@ public interface MediaPeriod extends SequenceableLoader { /** * Discards buffered media up to the specified position. + *

        + * This method should only be called after the period has been prepared. * * @param positionUs The position in microseconds. */ @@ -118,6 +120,8 @@ public interface MediaPeriod extends SequenceableLoader { *

        * After this method has returned a value other than {@link C#TIME_UNSET}, all * {@link SampleStream}s provided by the period are guaranteed to start from a key frame. + *

        + * This method should only be called after the period has been prepared. * * @return If a discontinuity was read then the playback position in microseconds after the * discontinuity. Else {@link C#TIME_UNSET}. From 1f815db367b5ca7112cd28f48c6879677379ae1e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 29 Jun 2017 04:22:22 -0700 Subject: [PATCH 192/220] Switch the IMA extension to use in-period ads This also adds support for seeking in periods with midroll ads. Remove Timeline.Period.isAd. Issue: #2617 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160510702 --- extensions/ima/README.md | 3 - .../exoplayer2/ext/ima/AdTimeline.java | 275 --------------- .../exoplayer2/ext/ima/ImaAdsLoader.java | 322 +++++++++--------- .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 317 ++++------------- .../ext/ima/SinglePeriodAdTimeline.java | 92 +++++ .../android/exoplayer2/ExoPlayerTest.java | 2 +- .../android/exoplayer2/TimelineTest.java | 2 +- .../java/com/google/android/exoplayer2/C.java | 8 +- .../google/android/exoplayer2/Timeline.java | 18 +- .../source/SinglePeriodTimeline.java | 2 +- .../source/dash/DashMediaSource.java | 2 +- .../google/android/exoplayer2/ui/TimeBar.java | 10 +- 12 files changed, 334 insertions(+), 719 deletions(-) delete mode 100644 extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTimeline.java create mode 100644 extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java diff --git a/extensions/ima/README.md b/extensions/ima/README.md index aaae44edcf..9ef37170d7 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -36,9 +36,6 @@ section of the app. This is a preview version with some known issues: -* Seeking is not yet ad aware. This means that it's possible to seek back into - ads that have already been played, and also seek past midroll ads without - them being played. Seeking will be made ad aware for the first stable release. * Midroll ads are not yet fully supported. `playAd` and `AD_STARTED` events are sometimes delayed, meaning that midroll ads take a long time to start and the ad overlay does not show immediately. diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTimeline.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTimeline.java deleted file mode 100644 index 1f8008ed10..0000000000 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTimeline.java +++ /dev/null @@ -1,275 +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.ext.ima; - -import android.util.Pair; -import com.google.ads.interactivemedia.v3.api.Ad; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; -import java.util.ArrayList; - -/** - * A {@link Timeline} for {@link ImaAdsMediaSource}. - */ -/* package */ final class AdTimeline extends Timeline { - - private static final Object AD_ID = new Object(); - - /** - * Builder for ad timelines. - */ - public static final class Builder { - - private final Timeline contentTimeline; - private final long contentDurationUs; - private final ArrayList isAd; - private final ArrayList ads; - private final ArrayList startTimesUs; - private final ArrayList endTimesUs; - private final ArrayList uids; - - /** - * Creates a new ad timeline builder using the specified {@code contentTimeline} as the timeline - * of the content within which to insert ad breaks. - * - * @param contentTimeline The timeline of the content within which to insert ad breaks. - */ - public Builder(Timeline contentTimeline) { - this.contentTimeline = contentTimeline; - contentDurationUs = contentTimeline.getPeriod(0, new Period()).durationUs; - isAd = new ArrayList<>(); - ads = new ArrayList<>(); - startTimesUs = new ArrayList<>(); - endTimesUs = new ArrayList<>(); - uids = new ArrayList<>(); - } - - /** - * Adds an ad period. Each individual ad in an ad pod is represented by a separate ad period. - * - * @param ad The {@link Ad} instance representing the ad break, or {@code null} if not known. - * @param adBreakIndex The index of the ad break that contains the ad in the timeline. - * @param adIndexInAdBreak The index of the ad in its ad break. - * @param durationUs The duration of the ad, in microseconds. May be {@link C#TIME_UNSET}. - * @return The builder. - */ - public Builder addAdPeriod(Ad ad, int adBreakIndex, int adIndexInAdBreak, long durationUs) { - isAd.add(true); - ads.add(ad); - startTimesUs.add(0L); - endTimesUs.add(durationUs); - uids.add(Pair.create(adBreakIndex, adIndexInAdBreak)); - return this; - } - - /** - * Adds a content period. - * - * @param startTimeUs The start time of the period relative to the start of the content - * timeline, in microseconds. - * @param endTimeUs The end time of the period relative to the start of the content timeline, in - * microseconds. May be {@link C#TIME_UNSET} to include the rest of the content. - * @return The builder. - */ - public Builder addContent(long startTimeUs, long endTimeUs) { - ads.add(null); - isAd.add(false); - startTimesUs.add(startTimeUs); - endTimesUs.add(endTimeUs == C.TIME_UNSET ? contentDurationUs : endTimeUs); - uids.add(Pair.create(startTimeUs, endTimeUs)); - return this; - } - - /** - * Builds and returns the ad timeline. - */ - public AdTimeline build() { - int periodCount = uids.size(); - Assertions.checkState(periodCount > 0); - Ad[] ads = new Ad[periodCount]; - boolean[] isAd = new boolean[periodCount]; - long[] startTimesUs = new long[periodCount]; - long[] endTimesUs = new long[periodCount]; - for (int i = 0; i < periodCount; i++) { - ads[i] = this.ads.get(i); - isAd[i] = this.isAd.get(i); - startTimesUs[i] = this.startTimesUs.get(i); - endTimesUs[i] = this.endTimesUs.get(i); - } - Object[] uids = this.uids.toArray(new Object[periodCount]); - return new AdTimeline(contentTimeline, isAd, ads, startTimesUs, endTimesUs, uids); - } - - } - - private final Period contentPeriod; - private final Window contentWindow; - private final boolean[] isAd; - private final Ad[] ads; - private final long[] startTimesUs; - private final long[] endTimesUs; - private final Object[] uids; - - private AdTimeline(Timeline contentTimeline, boolean[] isAd, Ad[] ads, long[] startTimesUs, - long[] endTimesUs, Object[] uids) { - contentWindow = contentTimeline.getWindow(0, new Window(), true); - contentPeriod = contentTimeline.getPeriod(0, new Period(), true); - this.isAd = isAd; - this.ads = ads; - this.startTimesUs = startTimesUs; - this.endTimesUs = endTimesUs; - this.uids = uids; - } - - /** - * Returns whether the period at {@code index} contains ad media. - */ - public boolean isPeriodAd(int index) { - return isAd[index]; - } - - /** - * Returns the duration of the content within which ads have been inserted, in microseconds. - */ - public long getContentDurationUs() { - return contentPeriod.durationUs; - } - - /** - * Returns the start time of the period at {@code periodIndex} relative to the start of the - * content, in microseconds. - * - * @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not a content - * period. - */ - public long getContentStartTimeUs(int periodIndex) { - Assertions.checkArgument(!isAd[periodIndex]); - return startTimesUs[periodIndex]; - } - - /** - * Returns the end time of the period at {@code periodIndex} relative to the start of the content, - * in microseconds. - * - * @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not a content - * period. - */ - public long getContentEndTimeUs(int periodIndex) { - Assertions.checkArgument(!isAd[periodIndex]); - return endTimesUs[periodIndex]; - } - - /** - * Returns the index of the ad break to which the period at {@code periodIndex} belongs. - * - * @param periodIndex The period index. - * @return The index of the ad break to which the period belongs. - * @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not an ad. - */ - public int getAdBreakIndex(int periodIndex) { - Assertions.checkArgument(isAd[periodIndex]); - int adBreakIndex = 0; - for (int i = 1; i < periodIndex; i++) { - if (!isAd[i] && isAd[i - 1]) { - adBreakIndex++; - } - } - return adBreakIndex; - } - - /** - * Returns the index of the ad at {@code periodIndex} in its ad break. - * - * @param periodIndex The period index. - * @return The index of the ad at {@code periodIndex} in its ad break. - * @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not an ad. - */ - public int getAdIndexInAdBreak(int periodIndex) { - Assertions.checkArgument(isAd[periodIndex]); - int adIndex = 0; - for (int i = 0; i < periodIndex; i++) { - if (isAd[i]) { - adIndex++; - } else { - adIndex = 0; - } - } - return adIndex; - } - - @Override - public int getWindowCount() { - return uids.length; - } - - @Override - public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { - if (repeatMode == ExoPlayer.REPEAT_MODE_ONE) { - repeatMode = ExoPlayer.REPEAT_MODE_ALL; - } - return super.getNextWindowIndex(windowIndex, repeatMode); - } - - @Override - public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { - if (repeatMode == ExoPlayer.REPEAT_MODE_ONE) { - repeatMode = ExoPlayer.REPEAT_MODE_ALL; - } - return super.getPreviousWindowIndex(windowIndex, repeatMode); - } - - @Override - public Window getWindow(int index, Window window, boolean setIds, - long defaultPositionProjectionUs) { - long startTimeUs = startTimesUs[index]; - long durationUs = endTimesUs[index] - startTimeUs; - if (isAd[index]) { - window.set(ads[index], C.TIME_UNSET, C.TIME_UNSET, false, false, 0L, durationUs, index, index, - 0L); - } else { - window.set(contentWindow.id, contentWindow.presentationStartTimeMs + C.usToMs(startTimeUs), - contentWindow.windowStartTimeMs + C.usToMs(startTimeUs), contentWindow.isSeekable, false, - 0L, durationUs, index, index, 0L); - } - return window; - } - - @Override - public int getPeriodCount() { - return uids.length; - } - - @Override - public Period getPeriod(int index, Period period, boolean setIds) { - Object id = setIds ? (isAd[index] ? AD_ID : contentPeriod.id) : null; - return period.set(id, uids[index], index, endTimesUs[index] - startTimesUs[index], 0, - isAd[index]); - } - - @Override - public int getIndexOfPeriod(Object uid) { - for (int i = 0; i < uids.length; i++) { - if (Util.areEqual(uid, uids[i])) { - return i; - } - } - return C.INDEX_UNSET; - } - -} 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 c4b626e355..0b14f16256 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 @@ -66,36 +66,35 @@ import java.util.List; public interface EventListener { /** - * Called when the timestamps of ad breaks are known. + * Called when the times of ad groups are known. * - * @param adBreakTimesUs The times of ad breaks, in microseconds. + * @param adGroupTimesUs The times of ad groups, in microseconds. */ - void onAdBreakTimesUsLoaded(long[] adBreakTimesUs); + void onAdGroupTimesUsLoaded(long[] adGroupTimesUs); + + /** + * Called when an ad group has been played to the end. + * + * @param adGroupIndex The index of the ad group. + */ + void onAdGroupPlayedToEnd(int adGroupIndex); /** * Called when the URI for the media of an ad has been loaded. * - * @param adBreakIndex The index of the ad break containing the ad with the media URI. - * @param adIndexInAdBreak The index of the ad in its ad break. + * @param adGroupIndex The index of the ad group containing the ad with the media URI. + * @param adIndexInAdGroup The index of the ad in its ad group. * @param uri The URI for the ad's media. */ - void onUriLoaded(int adBreakIndex, int adIndexInAdBreak, Uri uri); + void onAdUriLoaded(int adGroupIndex, int adIndexInAdGroup, Uri uri); /** - * Called when the {@link Ad} instance for a specified ad has been loaded. + * Called when an ad group has loaded. * - * @param adBreakIndex The index of the ad break containing the ad. - * @param adIndexInAdBreak The index of the ad in its ad break. - * @param ad The {@link Ad} instance for the ad. + * @param adGroupIndex The index of the ad group containing the ad. + * @param adCountInAdGroup The number of ads in the ad group. */ - void onAdLoaded(int adBreakIndex, int adIndexInAdBreak, Ad ad); - - /** - * Called when the specified ad break has been played to the end. - * - * @param adBreakIndex The index of the ad break. - */ - void onAdBreakPlayedToEnd(int adBreakIndex); + void onAdGroupLoaded(int adGroupIndex, int adCountInAdGroup); /** * Called when there was an error loading ads. @@ -127,51 +126,45 @@ import java.util.List; private final AdsLoader adsLoader; private AdsManager adsManager; - private AdTimeline adTimeline; + private long[] adGroupTimesUs; + private int[] adsLoadedInAdGroup; + private Timeline timeline; private long contentDurationMs; - private int lastContentPeriodIndex; - - private int playerPeriodIndex; private boolean released; // Fields tracking IMA's state. /** - * The index of the current ad break that IMA is loading. + * The index of the current ad group that IMA is loading. */ - private int adBreakIndex; + private int adGroupIndex; /** - * The index of the ad within its ad break, in {@link #loadAd(String)}. - */ - private int adIndexInAdBreak; - /** - * The total number of ads in the current ad break, or {@link C#INDEX_UNSET} if unknown. - */ - private int adCountInAdBreak; - - /** - * Tracks the period currently being played in IMA's model of playback. - */ - private int imaPeriodIndex; - /** - * Whether the period at {@link #imaPeriodIndex} is an ad. - */ - private boolean isAdDisplayed; - /** - * Whether {@link AdsLoader#contentComplete()} has been called since starting ad playback. - */ - private boolean sentContentComplete; - /** - * If {@link #isAdDisplayed} is set, stores whether IMA has called {@link #playAd()} and not + * If {@link #playingAdGroupIndex} is set, stores whether IMA has called {@link #playAd()} and not * {@link #stopAd()}. */ private boolean playingAd; /** - * If {@link #isAdDisplayed} is set, stores whether IMA has called {@link #pauseAd()} since a - * preceding call to {@link #playAd()} for the current ad. + * If {@link #playingAdGroupIndex} is set, stores whether IMA has called {@link #pauseAd()} since + * a preceding call to {@link #playAd()} for the current ad. */ private boolean pausedInAd; + /** + * Whether {@link AdsLoader#contentComplete()} has been called since starting ad playback. + */ + private boolean sentContentComplete; + + // Fields tracking the player/loader state. + + /** + * If the player is playing an ad, stores the ad group index. {@link C#INDEX_UNSET} otherwise. + */ + private int playingAdGroupIndex; + /** + * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET} + * otherwise. + */ + private int playingAdIndexInAdGroup; /** * If a content period has finished but IMA has not yet sent an ad event with * {@link AdEvent.AdEventType#CONTENT_PAUSE_REQUESTED}, stores the value of @@ -179,6 +172,14 @@ import java.util.List; * determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise. */ private long fakeContentProgressElapsedRealtimeMs; + /** + * Stores the pending content position when a seek operation was intercepted to play an ad. + */ + private long pendingContentPositionMs; + /** + * Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. + */ + private boolean sentPendingContentPositionMs; /** * Creates a new IMA ads loader. @@ -190,8 +191,7 @@ import java.util.List; * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to * use the default settings. If set, the player type and version fields may be overwritten. - * @param player The player instance that will play the loaded ad schedule. The player's timeline - * must be an {@link AdTimeline} matching the loaded ad schedule. + * @param player The player instance that will play the loaded ad schedule. * @param eventListener Listener for ad loader events. */ public ImaAdsLoader(Context context, Uri adTagUri, ViewGroup adUiViewGroup, @@ -201,9 +201,10 @@ import java.util.List; period = new Timeline.Period(); adCallbacks = new ArrayList<>(1); - lastContentPeriodIndex = C.INDEX_UNSET; - adCountInAdBreak = C.INDEX_UNSET; fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; + pendingContentPositionMs = C.TIME_UNSET; + adGroupIndex = C.INDEX_UNSET; + contentDurationMs = C.TIME_UNSET; player.addListener(this); @@ -262,13 +263,16 @@ import java.util.List; Log.d(TAG, "Initialized without preloading"); } } - eventListener.onAdBreakTimesUsLoaded(getAdBreakTimesUs(adsManager.getAdCuePoints())); + adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); + adsLoadedInAdGroup = new int[adGroupTimesUs.length]; + eventListener.onAdGroupTimesUsLoaded(adGroupTimesUs); } // AdEvent.AdEventListener implementation. @Override public void onAdEvent(AdEvent adEvent) { + Ad ad = adEvent.getAd(); if (DEBUG) { Log.d(TAG, "onAdEvent " + adEvent.getType()); } @@ -278,20 +282,18 @@ import java.util.List; } switch (adEvent.getType()) { case LOADED: - adsManager.start(); - break; - case STARTED: - // Note: This event is sometimes delivered several seconds after playAd is called. - // See [Internal: b/37775441]. - Ad ad = adEvent.getAd(); + // The ad position is not always accurate when using preloading. See [Internal: b/62613240]. AdPodInfo adPodInfo = ad.getAdPodInfo(); - adCountInAdBreak = adPodInfo.getTotalAds(); + int podIndex = adPodInfo.getPodIndex(); + adGroupIndex = podIndex == -1 ? adGroupTimesUs.length - 1 : podIndex; int adPosition = adPodInfo.getAdPosition(); - eventListener.onAdLoaded(adBreakIndex, adPosition - 1, ad); + int adCountInAdGroup = adPodInfo.getTotalAds(); + adsManager.start(); if (DEBUG) { - Log.d(TAG, "Started ad " + adPosition + " of " + adCountInAdBreak + " in ad break " - + adBreakIndex); + Log.d(TAG, "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in ad group " + + adGroupIndex); } + eventListener.onAdGroupLoaded(adGroupIndex, adCountInAdGroup); break; case CONTENT_PAUSE_REQUESTED: // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads @@ -325,40 +327,41 @@ import java.util.List; @Override public VideoProgressUpdate getContentProgress() { - if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { - long contentEndTimeMs = C.usToMs(adTimeline.getContentEndTimeUs(imaPeriodIndex)); - long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; - return new VideoProgressUpdate(contentEndTimeMs + elapsedSinceEndMs, contentDurationMs); + if (pendingContentPositionMs != C.TIME_UNSET) { + sentPendingContentPositionMs = true; + return new VideoProgressUpdate(pendingContentPositionMs, contentDurationMs); } - - if (adTimeline == null || isAdDisplayed || imaPeriodIndex != playerPeriodIndex - || contentDurationMs == C.TIME_UNSET) { + if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { + long adGroupTimeMs = C.usToMs(adGroupTimesUs[adGroupIndex]); + if (adGroupTimeMs == C.TIME_END_OF_SOURCE) { + adGroupTimeMs = contentDurationMs; + } + long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; + return new VideoProgressUpdate(adGroupTimeMs + elapsedSinceEndMs, contentDurationMs); + } + if (player.isPlayingAd() || contentDurationMs == C.TIME_UNSET) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } - checkForContentComplete(); - long positionMs = C.usToMs(adTimeline.getContentStartTimeUs(imaPeriodIndex)) - + player.getCurrentPosition(); - return new VideoProgressUpdate(positionMs, contentDurationMs); + return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs); } // VideoAdPlayer implementation. @Override public VideoProgressUpdate getAdProgress() { - if (adTimeline == null || !isAdDisplayed || imaPeriodIndex != playerPeriodIndex - || adTimeline.getPeriod(imaPeriodIndex, period).getDurationUs() == C.TIME_UNSET) { + if (!player.isPlayingAd()) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } - return new VideoProgressUpdate(player.getCurrentPosition(), period.getDurationMs()); + return new VideoProgressUpdate(player.getCurrentPosition(), player.getDuration()); } @Override public void loadAd(String adUriString) { + int adIndexInAdGroup = adsLoadedInAdGroup[adGroupIndex]++; if (DEBUG) { - Log.d(TAG, "loadAd at index " + adIndexInAdBreak + " in ad break " + adBreakIndex); + Log.d(TAG, "loadAd at index " + adIndexInAdGroup + " in ad group " + adGroupIndex); } - eventListener.onUriLoaded(adBreakIndex, adIndexInAdBreak, Uri.parse(adUriString)); - adIndexInAdBreak++; + eventListener.onAdUriLoaded(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString)); } @Override @@ -376,7 +379,6 @@ import java.util.List; if (DEBUG) { Log.d(TAG, "playAd"); } - Assertions.checkState(isAdDisplayed); if (playingAd && !pausedInAd) { // Work around an issue where IMA does not always call stopAd before resuming content. // See [Internal: b/38354028]. @@ -443,18 +445,12 @@ import java.util.List; // The player is being re-prepared and this source will be released. return; } - if (adTimeline == null) { - // TODO: Handle initial seeks after the first period. - isAdDisplayed = timeline.getPeriod(0, period).isAd; - imaPeriodIndex = 0; - player.seekTo(0, 0); - } - adTimeline = (AdTimeline) timeline; - contentDurationMs = C.usToMs(adTimeline.getContentDurationUs()); - lastContentPeriodIndex = adTimeline.getPeriodCount() - 1; - while (adTimeline.isPeriodAd(lastContentPeriodIndex)) { - // All timelines have at least one content period. - lastContentPeriodIndex--; + Assertions.checkArgument(timeline.getPeriodCount() == 1); + this.timeline = timeline; + contentDurationMs = C.usToMs(timeline.getPeriod(0, period).durationUs); + if (player.isPlayingAd()) { + playingAdGroupIndex = player.getCurrentAdGroupIndex(); + playingAdIndexInAdGroup = player.getCurrentAdIndexInAdGroup(); } } @@ -470,9 +466,9 @@ import java.util.List; @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == ExoPlayer.STATE_BUFFERING && playWhenReady) { + if (!playingAd && playbackState == ExoPlayer.STATE_BUFFERING && playWhenReady) { checkForContentComplete(); - } else if (playbackState == ExoPlayer.STATE_ENDED && isAdDisplayed) { + } else if (playingAd && playbackState == ExoPlayer.STATE_ENDED) { // IMA is waiting for the ad playback to finish so invoke the callback now. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. for (VideoAdPlayerCallback callback : adCallbacks) { @@ -488,7 +484,7 @@ import java.util.List; @Override public void onPlayerError(ExoPlaybackException error) { - if (isAdDisplayed && adTimeline.isPeriodAd(playerPeriodIndex)) { + if (player.isPlayingAd()) { for (VideoAdPlayerCallback callback : adCallbacks) { callback.onError(); } @@ -497,23 +493,41 @@ import java.util.List; @Override public void onPositionDiscontinuity() { - if (player.getCurrentPeriodIndex() == playerPeriodIndex + 1) { - if (isAdDisplayed) { - // IMA is waiting for the ad playback to finish so invoke the callback now. - // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. - for (VideoAdPlayerCallback callback : adCallbacks) { - callback.onEnded(); - } - } else { - player.setPlayWhenReady(false); - if (imaPeriodIndex == playerPeriodIndex) { - // IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position. - Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET); - fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); + if (!player.isPlayingAd() && playingAdGroupIndex == C.INDEX_UNSET) { + long positionUs = C.msToUs(player.getCurrentPosition()); + int adGroupIndex = timeline.getPeriod(0, period).getAdGroupIndexForPositionUs(positionUs); + if (adGroupIndex != C.INDEX_UNSET) { + sentPendingContentPositionMs = false; + pendingContentPositionMs = player.getCurrentPosition(); + } + return; + } + + boolean adFinished = (!player.isPlayingAd() && playingAdGroupIndex != C.INDEX_UNSET) + || (player.isPlayingAd() && playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup()); + if (adFinished) { + // IMA is waiting for the ad playback to finish so invoke the callback now. + // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. + for (VideoAdPlayerCallback callback : adCallbacks) { + callback.onEnded(); + } + } + + if (player.isPlayingAd() && playingAdGroupIndex == C.INDEX_UNSET) { + player.setPlayWhenReady(false); + // IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position. + Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET); + fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); + if (adGroupIndex == adGroupTimesUs.length - 1) { + adsLoader.contentComplete(); + if (DEBUG) { + Log.d(TAG, "adsLoader.contentComplete"); } } } - playerPeriodIndex = player.getCurrentPeriodIndex(); + boolean isPlayingAd = player.isPlayingAd(); + playingAdGroupIndex = isPlayingAd ? player.getCurrentAdGroupIndex() : C.INDEX_UNSET; + playingAdIndexInAdGroup = isPlayingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; } @Override @@ -527,70 +541,42 @@ import java.util.List; * Resumes the player, ensuring the current period is a content period by seeking if necessary. */ private void resumeContentInternal() { - if (adTimeline != null) { - if (imaPeriodIndex < lastContentPeriodIndex) { - if (playingAd) { - // Work around an issue where IMA does not always call stopAd before resuming content. - // See [Internal: b/38354028]. - if (DEBUG) { - Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd"); - } - stopAdInternal(); + if (contentDurationMs != C.TIME_UNSET) { + if (playingAd) { + // Work around an issue where IMA does not always call stopAd before resuming content. + // See [Internal: b/38354028]. + if (DEBUG) { + Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd"); } - while (adTimeline.isPeriodAd(imaPeriodIndex)) { - imaPeriodIndex++; - } - synchronizePlayerToIma(); + stopAdInternal(); } } player.setPlayWhenReady(true); + clearFlags(); } - /** - * Pauses the player, and ensures that the current period is an ad period by seeking if necessary. - */ private void pauseContentInternal() { + if (sentPendingContentPositionMs) { + pendingContentPositionMs = C.TIME_UNSET; + sentPendingContentPositionMs = false; + } // IMA is requesting to pause content, so stop faking the content position. fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - if (adTimeline != null && !isAdDisplayed) { - // Seek to the next ad. - while (!adTimeline.isPeriodAd(imaPeriodIndex)) { - imaPeriodIndex++; - } - synchronizePlayerToIma(); - } else { - // IMA is sending an initial CONTENT_PAUSE_REQUESTED before a pre-roll ad. - Assertions.checkState(playerPeriodIndex == 0 && imaPeriodIndex == 0); - } player.setPlayWhenReady(false); + clearFlags(); } - /** - * Stops the currently playing ad, seeking to the next content period if there is one. May only be - * called when {@link #playingAd} is {@code true}. - */ private void stopAdInternal() { Assertions.checkState(playingAd); - if (imaPeriodIndex != adTimeline.getPeriodCount() - 1) { - player.setPlayWhenReady(false); - imaPeriodIndex++; - if (!adTimeline.isPeriodAd(imaPeriodIndex)) { - eventListener.onAdBreakPlayedToEnd(adBreakIndex); - adBreakIndex++; - adIndexInAdBreak = 0; - } - synchronizePlayerToIma(); - } else { - eventListener.onAdBreakPlayedToEnd(adTimeline.getAdBreakIndex(imaPeriodIndex)); + player.setPlayWhenReady(false); + if (!player.isPlayingAd()) { + eventListener.onAdGroupPlayedToEnd(adGroupIndex); + adGroupIndex = C.INDEX_UNSET; } + clearFlags(); } - private void synchronizePlayerToIma() { - if (playerPeriodIndex != imaPeriodIndex) { - player.seekTo(imaPeriodIndex, 0); - } - - isAdDisplayed = adTimeline.isPeriodAd(imaPeriodIndex); + private void clearFlags() { // If an ad is displayed, these flags will be updated in response to playAd/pauseAd/stopAd until // the content is resumed. playingAd = false; @@ -598,14 +584,9 @@ import java.util.List; } private void checkForContentComplete() { - if (adTimeline == null || isAdDisplayed || sentContentComplete) { - return; - } - long positionMs = C.usToMs(adTimeline.getContentStartTimeUs(imaPeriodIndex)) - + player.getCurrentPosition(); - if (playerPeriodIndex == lastContentPeriodIndex - && positionMs + END_OF_CONTENT_POSITION_THRESHOLD_MS - >= C.usToMs(adTimeline.getContentEndTimeUs(playerPeriodIndex))) { + if (contentDurationMs != C.TIME_UNSET + && player.getCurrentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs + && !sentContentComplete) { adsLoader.contentComplete(); if (DEBUG) { Log.d(TAG, "adsLoader.contentComplete"); @@ -614,19 +595,20 @@ import java.util.List; } } - private static long[] getAdBreakTimesUs(List cuePoints) { + private static long[] getAdGroupTimesUs(List cuePoints) { if (cuePoints.isEmpty()) { - // If no cue points are specified, there is a preroll ad break. + // If no cue points are specified, there is a preroll ad. return new long[] {0}; } int count = cuePoints.size(); - long[] adBreakTimesUs = new long[count]; + long[] adGroupTimesUs = new long[count]; for (int i = 0; i < count; i++) { double cuePoint = cuePoints.get(i); - adBreakTimesUs[i] = cuePoint == -1.0 ? C.TIME_UNSET : (long) (C.MICROS_PER_SECOND * cuePoint); + adGroupTimesUs[i] = + cuePoint == -1.0 ? C.TIME_END_OF_SOURCE : (long) (C.MICROS_PER_SECOND * cuePoint); } - return adBreakTimesUs; + return adGroupTimesUs; } } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index ea6aaaf01c..5e96bd26dc 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -20,20 +20,14 @@ import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.view.ViewGroup; -import com.google.ads.interactivemedia.v3.api.Ad; -import com.google.ads.interactivemedia.v3.api.AdPodInfo; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.source.ClippingMediaPeriod; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.SampleStream; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; @@ -56,7 +50,8 @@ public final class ImaAdsMediaSource implements MediaSource { private final ImaSdkSettings imaSdkSettings; private final Handler mainHandler; private final AdListener adLoaderListener; - private final Map mediaSourceByMediaPeriod; + private final Map adMediaSourceByMediaPeriod; + private final Timeline.Period period; private Handler playerHandler; private ExoPlayer player; @@ -65,13 +60,12 @@ public final class ImaAdsMediaSource implements MediaSource { // Accessed on the player thread. private Timeline contentTimeline; private Object contentManifest; - private long[] adBreakTimesUs; - private boolean[] playedAdBreak; - private Ad[][] adBreakAds; - private Timeline[][] adBreakTimelines; - private MediaSource[][] adBreakMediaSources; - private DeferredMediaPeriod[][] adBreakDeferredMediaPeriods; - private AdTimeline timeline; + private long[] adGroupTimesUs; + private boolean[] hasPlayedAdGroup; + private int[] adCounts; + private MediaSource[][] adGroupMediaSources; + private boolean[][] isAdAvailable; + private long[][] adDurationsUs; private MediaSource.Listener listener; private IOException adLoadError; @@ -120,8 +114,11 @@ public final class ImaAdsMediaSource implements MediaSource { this.imaSdkSettings = imaSdkSettings; mainHandler = new Handler(Looper.getMainLooper()); adLoaderListener = new AdListener(); - mediaSourceByMediaPeriod = new HashMap<>(); - adBreakMediaSources = new MediaSource[0][]; + adMediaSourceByMediaPeriod = new HashMap<>(); + period = new Timeline.Period(); + adGroupMediaSources = new MediaSource[0][]; + isAdAvailable = new boolean[0][]; + adDurationsUs = new long[0][]; } @Override @@ -151,7 +148,7 @@ public final class ImaAdsMediaSource implements MediaSource { throw adLoadError; } contentMediaSource.maybeThrowSourceInfoRefreshError(); - for (MediaSource[] mediaSources : adBreakMediaSources) { + for (MediaSource[] mediaSources : adGroupMediaSources) { for (MediaSource mediaSource : mediaSources) { mediaSource.maybeThrowSourceInfoRefreshError(); } @@ -160,49 +157,23 @@ public final class ImaAdsMediaSource implements MediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - int index = id.periodIndex; - if (timeline.isPeriodAd(index)) { - int adBreakIndex = timeline.getAdBreakIndex(index); - int adIndexInAdBreak = timeline.getAdIndexInAdBreak(index); - if (adIndexInAdBreak >= adBreakMediaSources[adBreakIndex].length) { - DeferredMediaPeriod deferredPeriod = new DeferredMediaPeriod(0, allocator); - if (adIndexInAdBreak >= adBreakDeferredMediaPeriods[adBreakIndex].length) { - adBreakDeferredMediaPeriods[adBreakIndex] = Arrays.copyOf( - adBreakDeferredMediaPeriods[adBreakIndex], adIndexInAdBreak + 1); - } - adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak] = deferredPeriod; - return deferredPeriod; - } - - MediaSource adBreakMediaSource = adBreakMediaSources[adBreakIndex][adIndexInAdBreak]; - MediaPeriod adBreakMediaPeriod = - adBreakMediaSource.createPeriod(new MediaPeriodId(0), allocator); - mediaSourceByMediaPeriod.put(adBreakMediaPeriod, adBreakMediaSource); - return adBreakMediaPeriod; + if (id.isAd()) { + MediaSource mediaSource = adGroupMediaSources[id.adGroupIndex][id.adIndexInAdGroup]; + MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator); + adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource); + return mediaPeriod; } else { - long startUs = timeline.getContentStartTimeUs(index); - long endUs = timeline.getContentEndTimeUs(index); - MediaPeriod contentMediaPeriod = - contentMediaSource.createPeriod(new MediaPeriodId(0), allocator); - ClippingMediaPeriod clippingPeriod = new ClippingMediaPeriod(contentMediaPeriod, true); - clippingPeriod.setClipping(startUs, endUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE : endUs); - mediaSourceByMediaPeriod.put(contentMediaPeriod, contentMediaSource); - return clippingPeriod; + return contentMediaSource.createPeriod(id, allocator); } } @Override public void releasePeriod(MediaPeriod mediaPeriod) { - if (mediaPeriod instanceof DeferredMediaPeriod) { - mediaPeriod = ((DeferredMediaPeriod) mediaPeriod).mediaPeriod; - if (mediaPeriod == null) { - // Nothing to do. - return; - } - } else if (mediaPeriod instanceof ClippingMediaPeriod) { - mediaPeriod = ((ClippingMediaPeriod) mediaPeriod).mediaPeriod; + if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) { + adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod); + } else { + contentMediaSource.releasePeriod(mediaPeriod); } - mediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod); } @Override @@ -210,7 +181,7 @@ public final class ImaAdsMediaSource implements MediaSource { released = true; adLoadError = null; contentMediaSource.releaseSource(); - for (MediaSource[] mediaSources : adBreakMediaSources) { + for (MediaSource[] mediaSources : adGroupMediaSources) { for (MediaSource mediaSource : mediaSources) { mediaSource.releaseSource(); } @@ -229,19 +200,19 @@ public final class ImaAdsMediaSource implements MediaSource { // Internal methods. - private void onAdBreakTimesUsLoaded(long[] adBreakTimesUs) { - Assertions.checkState(this.adBreakTimesUs == null); - this.adBreakTimesUs = adBreakTimesUs; - int adBreakCount = adBreakTimesUs.length; - adBreakAds = new Ad[adBreakCount][]; - Arrays.fill(adBreakAds, new Ad[0]); - adBreakTimelines = new Timeline[adBreakCount][]; - Arrays.fill(adBreakTimelines, new Timeline[0]); - adBreakMediaSources = new MediaSource[adBreakCount][]; - Arrays.fill(adBreakMediaSources, new MediaSource[0]); - adBreakDeferredMediaPeriods = new DeferredMediaPeriod[adBreakCount][]; - Arrays.fill(adBreakDeferredMediaPeriods, new DeferredMediaPeriod[0]); - playedAdBreak = new boolean[adBreakCount]; + private void onAdGroupTimesUsLoaded(long[] adGroupTimesUs) { + Assertions.checkState(this.adGroupTimesUs == null); + int adGroupCount = adGroupTimesUs.length; + this.adGroupTimesUs = adGroupTimesUs; + hasPlayedAdGroup = new boolean[adGroupCount]; + adCounts = new int[adGroupCount]; + Arrays.fill(adCounts, C.LENGTH_UNSET); + adGroupMediaSources = new MediaSource[adGroupCount][]; + Arrays.fill(adGroupMediaSources, new MediaSource[0]); + isAdAvailable = new boolean[adGroupCount][]; + Arrays.fill(isAdAvailable, new boolean[0]); + adDurationsUs = new long[adGroupCount][]; + Arrays.fill(adDurationsUs, new long[0]); maybeUpdateSourceInfo(); } @@ -251,98 +222,51 @@ public final class ImaAdsMediaSource implements MediaSource { maybeUpdateSourceInfo(); } - private void onAdUriLoaded(final int adBreakIndex, final int adIndexInAdBreak, Uri uri) { + private void onAdGroupPlayedToEnd(int adGroupIndex) { + hasPlayedAdGroup[adGroupIndex] = true; + maybeUpdateSourceInfo(); + } + + private void onAdUriLoaded(final int adGroupIndex, final int adIndexInAdGroup, Uri uri) { MediaSource adMediaSource = new ExtractorMediaSource(uri, dataSourceFactory, new DefaultExtractorsFactory(), mainHandler, adLoaderListener); - if (adBreakMediaSources[adBreakIndex].length <= adIndexInAdBreak) { - int adCount = adIndexInAdBreak + 1; - adBreakMediaSources[adBreakIndex] = Arrays.copyOf(adBreakMediaSources[adBreakIndex], adCount); - adBreakTimelines[adBreakIndex] = Arrays.copyOf(adBreakTimelines[adBreakIndex], adCount); - } - adBreakMediaSources[adBreakIndex][adIndexInAdBreak] = adMediaSource; - if (adIndexInAdBreak < adBreakDeferredMediaPeriods[adBreakIndex].length - && adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak] != null) { - adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak].setMediaSource( - adBreakMediaSources[adBreakIndex][adIndexInAdBreak]); - mediaSourceByMediaPeriod.put( - adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak].mediaPeriod, adMediaSource); + int oldAdCount = adGroupMediaSources[adGroupIndex].length; + if (adIndexInAdGroup >= oldAdCount) { + int adCount = adIndexInAdGroup + 1; + adGroupMediaSources[adGroupIndex] = Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount); + isAdAvailable[adGroupIndex] = Arrays.copyOf(isAdAvailable[adGroupIndex], adCount); + adDurationsUs[adGroupIndex] = Arrays.copyOf(adDurationsUs[adGroupIndex], adCount); + Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET); } + adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource; + isAdAvailable[adGroupIndex][adIndexInAdGroup] = true; adMediaSource.prepareSource(player, false, new Listener() { @Override public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - onAdSourceInfoRefreshed(adBreakIndex, adIndexInAdBreak, timeline); + onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline); } }); } - private void onAdSourceInfoRefreshed(int adBreakIndex, int adIndexInAdBreak, Timeline timeline) { - adBreakTimelines[adBreakIndex][adIndexInAdBreak] = timeline; + private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) { + Assertions.checkArgument(timeline.getPeriodCount() == 1); + adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs(); maybeUpdateSourceInfo(); } - private void onAdLoaded(int adBreakIndex, int adIndexInAdBreak, Ad ad) { - if (adBreakAds[adBreakIndex].length <= adIndexInAdBreak) { - int adCount = adIndexInAdBreak + 1; - adBreakAds[adBreakIndex] = Arrays.copyOf(adBreakAds[adBreakIndex], adCount); + private void onAdGroupLoaded(int adGroupIndex, int adCountInAdGroup) { + if (adCounts[adGroupIndex] == C.LENGTH_UNSET) { + adCounts[adGroupIndex] = adCountInAdGroup; + maybeUpdateSourceInfo(); } - adBreakAds[adBreakIndex][adIndexInAdBreak] = ad; - maybeUpdateSourceInfo(); } private void maybeUpdateSourceInfo() { - if (adBreakTimesUs == null || contentTimeline == null) { - // We don't have enough information to start building the timeline yet. - return; + if (adGroupTimesUs != null && contentTimeline != null) { + SinglePeriodAdTimeline timeline = new SinglePeriodAdTimeline(contentTimeline, adGroupTimesUs, + hasPlayedAdGroup, adCounts, isAdAvailable, adDurationsUs); + listener.onSourceInfoRefreshed(timeline, contentManifest); } - - AdTimeline.Builder builder = new AdTimeline.Builder(contentTimeline); - int count = adBreakTimesUs.length; - boolean preroll = adBreakTimesUs[0] == 0; - boolean postroll = adBreakTimesUs[count - 1] == C.TIME_UNSET; - int midrollCount = count - (preroll ? 1 : 0) - (postroll ? 1 : 0); - - int adBreakIndex = 0; - long contentTimeUs = 0; - if (preroll) { - addAdBreak(builder, adBreakIndex++); - } - for (int i = 0; i < midrollCount; i++) { - long startTimeUs = contentTimeUs; - contentTimeUs = adBreakTimesUs[adBreakIndex]; - builder.addContent(startTimeUs, contentTimeUs); - addAdBreak(builder, adBreakIndex++); - } - builder.addContent(contentTimeUs, C.TIME_UNSET); - if (postroll) { - addAdBreak(builder, adBreakIndex); - } - - timeline = builder.build(); - listener.onSourceInfoRefreshed(timeline, contentManifest); - } - - private void addAdBreak(AdTimeline.Builder builder, int adBreakIndex) { - int adCount = adBreakMediaSources[adBreakIndex].length; - AdPodInfo adPodInfo = null; - for (int adIndex = 0; adIndex < adCount; adIndex++) { - Timeline adTimeline = adBreakTimelines[adBreakIndex][adIndex]; - long adDurationUs = adTimeline != null - ? adTimeline.getPeriod(0, new Timeline.Period()).getDurationUs() : C.TIME_UNSET; - Ad ad = adIndex < adBreakAds[adBreakIndex].length - ? adBreakAds[adBreakIndex][adIndex] : null; - builder.addAdPeriod(ad, adBreakIndex, adIndex, adDurationUs); - if (ad != null) { - adPodInfo = ad.getAdPodInfo(); - } - } - if (adPodInfo == null || adPodInfo.getTotalAds() > adCount) { - // We don't know how many ads are in the ad break, or they have not loaded yet. - builder.addAdPeriod(null, adBreakIndex, adCount, C.TIME_UNSET); - } - } - - private void onAdBreakPlayedToEnd(int adBreakIndex) { - playedAdBreak[adBreakIndex] = true; } /** @@ -352,7 +276,7 @@ public final class ImaAdsMediaSource implements MediaSource { ExtractorMediaSource.EventListener { @Override - public void onAdBreakTimesUsLoaded(final long[] adBreakTimesUs) { + public void onAdGroupTimesUsLoaded(final long[] adGroupTimesUs) { if (released) { return; } @@ -362,13 +286,13 @@ public final class ImaAdsMediaSource implements MediaSource { if (released) { return; } - ImaAdsMediaSource.this.onAdBreakTimesUsLoaded(adBreakTimesUs); + ImaAdsMediaSource.this.onAdGroupTimesUsLoaded(adGroupTimesUs); } }); } @Override - public void onUriLoaded(final int adBreakIndex, final int adIndexInAdBreak, final Uri uri) { + public void onAdGroupPlayedToEnd(final int adGroupIndex) { if (released) { return; } @@ -378,13 +302,13 @@ public final class ImaAdsMediaSource implements MediaSource { if (released) { return; } - ImaAdsMediaSource.this.onAdUriLoaded(adBreakIndex, adIndexInAdBreak, uri); + ImaAdsMediaSource.this.onAdGroupPlayedToEnd(adGroupIndex); } }); } @Override - public void onAdLoaded(final int adBreakIndex, final int adIndexInAdBreak, final Ad ad) { + public void onAdUriLoaded(final int adGroupIndex, final int adIndexInAdGroup, final Uri uri) { if (released) { return; } @@ -394,13 +318,13 @@ public final class ImaAdsMediaSource implements MediaSource { if (released) { return; } - ImaAdsMediaSource.this.onAdLoaded(adBreakIndex, adIndexInAdBreak, ad); + ImaAdsMediaSource.this.onAdUriLoaded(adGroupIndex, adIndexInAdGroup, uri); } }); } @Override - public void onAdBreakPlayedToEnd(final int adBreakIndex) { + public void onAdGroupLoaded(final int adGroupIndex, final int adCountInAdGroup) { if (released) { return; } @@ -410,7 +334,7 @@ public final class ImaAdsMediaSource implements MediaSource { if (released) { return; } - ImaAdsMediaSource.this.onAdBreakPlayedToEnd(adBreakIndex); + ImaAdsMediaSource.this.onAdGroupLoaded(adGroupIndex, adCountInAdGroup); } }); } @@ -433,99 +357,4 @@ public final class ImaAdsMediaSource implements MediaSource { } - private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { - - private final int index; - private final Allocator allocator; - - public MediaPeriod mediaPeriod; - private MediaPeriod.Callback callback; - private long positionUs; - - public DeferredMediaPeriod(int index, Allocator allocator) { - this.index = index; - this.allocator = allocator; - } - - public void setMediaSource(MediaSource mediaSource) { - mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(index), allocator); - if (callback != null) { - mediaPeriod.prepare(this, positionUs); - } - } - - @Override - public void prepare(Callback callback, long positionUs) { - this.callback = callback; - this.positionUs = positionUs; - if (mediaPeriod != null) { - mediaPeriod.prepare(this, positionUs); - } - } - - @Override - public void maybeThrowPrepareError() throws IOException { - if (mediaPeriod != null) { - mediaPeriod.maybeThrowPrepareError(); - } - } - - @Override - public TrackGroupArray getTrackGroups() { - return mediaPeriod.getTrackGroups(); - } - - @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, - positionUs); - } - - @Override - public void discardBuffer(long positionUs) { - // Do nothing. - } - - @Override - public long readDiscontinuity() { - return mediaPeriod.readDiscontinuity(); - } - - @Override - public long getBufferedPositionUs() { - return mediaPeriod.getBufferedPositionUs(); - } - - @Override - public long seekToUs(long positionUs) { - return mediaPeriod.seekToUs(positionUs); - } - - @Override - public long getNextLoadPositionUs() { - return mediaPeriod.getNextLoadPositionUs(); - } - - @Override - public boolean continueLoading(long positionUs) { - return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); - } - - // MediaPeriod.Callback implementation. - - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - Assertions.checkArgument(this.mediaPeriod == mediaPeriod); - callback.onPrepared(this); - } - - @Override - public void onContinueLoadingRequested(MediaPeriod mediaPeriod) { - Assertions.checkArgument(this.mediaPeriod == mediaPeriod); - callback.onContinueLoadingRequested(this); - } - - } - } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java new file mode 100644 index 0000000000..78d3bb9e73 --- /dev/null +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java @@ -0,0 +1,92 @@ +/* + * 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.ext.ima; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Assertions; + +/** + * A {@link Timeline} for sources that have ads. + */ +public final class SinglePeriodAdTimeline extends Timeline { + + private final Timeline contentTimeline; + private final long[] adGroupTimesUs; + private final boolean[] hasPlayedAdGroup; + private final int[] adCounts; + private final boolean[][] isAdAvailable; + private final long[][] adDurationsUs; + + /** + * Creates a new timeline with a single period containing the specified ads. + * + * @param contentTimeline The timeline of the content alongside which ads will be played. It must + * have one window and one period. + * @param adGroupTimesUs The times of ad groups relative to the start of the period, in + * microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that + * the period has a postroll ad. + * @param hasPlayedAdGroup Whether each ad group has been played. + * @param adCounts The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET} + * if the number of ads is not yet known. + * @param isAdAvailable Whether each ad in each ad group is available. + * @param adDurationsUs The duration of each ad in each ad group, in microseconds. An element + * may be {@link C#TIME_UNSET} if the duration is not yet known. + */ + public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs, + boolean[] hasPlayedAdGroup, int[] adCounts, boolean[][] isAdAvailable, + long[][] adDurationsUs) { + Assertions.checkState(contentTimeline.getPeriodCount() == 1); + Assertions.checkState(contentTimeline.getWindowCount() == 1); + this.contentTimeline = contentTimeline; + this.adGroupTimesUs = adGroupTimesUs; + this.hasPlayedAdGroup = hasPlayedAdGroup; + this.adCounts = adCounts; + this.isAdAvailable = isAdAvailable; + this.adDurationsUs = adDurationsUs; + } + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + return contentTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); + } + + @Override + public int getPeriodCount() { + return 1; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + contentTimeline.getPeriod(periodIndex, period, setIds); + period.set(period.id, period.uid, period.windowIndex, period.durationUs, + period.getPositionInWindowUs(), adGroupTimesUs, hasPlayedAdGroup, adCounts, + isAdAvailable, adDurationsUs); + return period; + } + + @Override + public int getIndexOfPeriod(Object uid) { + return contentTimeline.getIndexOfPeriod(uid); + } + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 8d137fa71e..7bbb6f9306 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -483,7 +483,7 @@ public final class ExoPlayerTest extends TestCase { public Period getPeriod(int periodIndex, Period period, boolean setIds) { TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex]; Object id = setIds ? periodIndex : null; - return period.set(id, id, periodIndex, windowDefinition.durationUs, 0, false); + return period.set(id, id, periodIndex, windowDefinition.durationUs, 0); } @Override diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java index 15763ae66d..8b0504253b 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java @@ -63,7 +63,7 @@ public class TimelineTest extends TestCase { @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { - return period.set(new int[] { id, periodIndex }, null, 0, WINDOW_DURATION_US, 0, false); + return period.set(new int[] { id, periodIndex }, null, 0, WINDOW_DURATION_US, 0); } @Override 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 62afbc98a7..e8c47d9811 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 @@ -758,24 +758,24 @@ public final class C { /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving - * {@link #TIME_UNSET} values. + * {@link #TIME_UNSET} and {@link #TIME_END_OF_SOURCE} values. * * @param timeUs The time in microseconds. * @return The corresponding time in milliseconds. */ public static long usToMs(long timeUs) { - return timeUs == TIME_UNSET ? TIME_UNSET : (timeUs / 1000); + return (timeUs == TIME_UNSET || timeUs == TIME_END_OF_SOURCE) ? timeUs : (timeUs / 1000); } /** * Converts a time in milliseconds to the corresponding time in microseconds, preserving - * {@link #TIME_UNSET} values. + * {@link #TIME_UNSET} values and {@link #TIME_END_OF_SOURCE} values. * * @param timeMs The time in milliseconds. * @return The corresponding time in microseconds. */ public static long msToUs(long timeMs) { - return timeMs == TIME_UNSET ? TIME_UNSET : (timeMs * 1000); + return (timeMs == TIME_UNSET || timeMs == TIME_END_OF_SOURCE) ? timeMs : (timeMs * 1000); } /** 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 a8f66231c1..19e66f9031 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 @@ -264,13 +264,6 @@ public abstract class Timeline { */ public long durationUs; - // TODO: Remove this flag now that in-period ads are supported. - - /** - * Whether this period contains an ad. - */ - public boolean isAd; - private long positionInWindowUs; private long[] adGroupTimesUs; private boolean[] hasPlayedAdGroup; @@ -289,12 +282,11 @@ public abstract class Timeline { * @param positionInWindowUs The position of the start of this period relative to the start of * the window to which it belongs, in milliseconds. May be negative if the start of the * period is not within the window. - * @param isAd Whether this period is an ad. * @return This period, for convenience. */ public Period set(Object id, Object uid, int windowIndex, long durationUs, - long positionInWindowUs, boolean isAd) { - return set(id, uid, windowIndex, durationUs, positionInWindowUs, isAd, null, null, null, null, + long positionInWindowUs) { + return set(id, uid, windowIndex, durationUs, positionInWindowUs, null, null, null, null, null); } @@ -309,7 +301,6 @@ public abstract class Timeline { * @param positionInWindowUs The position of the start of this period relative to the start of * the window to which it belongs, in milliseconds. May be negative if the start of the * period is not within the window. - * @param isAd Whether this period is an ad. * @param adGroupTimesUs The times of ad groups relative to the start of the period, in * microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that * the period has a postroll ad. @@ -322,14 +313,13 @@ public abstract class Timeline { * @return This period, for convenience. */ public Period set(Object id, Object uid, int windowIndex, long durationUs, - long positionInWindowUs, boolean isAd, long[] adGroupTimesUs, boolean[] hasPlayedAdGroup, - int[] adCounts, boolean[][] isAdAvailable, long[][] adDurationsUs) { + long positionInWindowUs, long[] adGroupTimesUs, boolean[] hasPlayedAdGroup, int[] adCounts, + boolean[][] isAdAvailable, long[][] adDurationsUs) { this.id = id; this.uid = uid; this.windowIndex = windowIndex; this.durationUs = durationUs; this.positionInWindowUs = positionInWindowUs; - this.isAd = isAd; this.adGroupTimesUs = adGroupTimesUs; this.hasPlayedAdGroup = hasPlayedAdGroup; this.adCounts = adCounts; 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 447839392e..ae367ef14c 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 @@ -99,7 +99,7 @@ public final class SinglePeriodTimeline extends Timeline { public Period getPeriod(int periodIndex, Period period, boolean setIds) { Assertions.checkIndex(periodIndex, 0, 1); Object id = setIds ? ID : null; - return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs, false); + return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs); } @Override 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 f1d5ad96fa..e17f1d26e7 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 @@ -653,7 +653,7 @@ public final class DashMediaSource implements MediaSource { + Assertions.checkIndex(periodIndex, 0, manifest.getPeriodCount()) : null; return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex), C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs) - - offsetInFirstPeriodUs, false); + - offsetInFirstPeriodUs); } @Override diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java index 215688083d..44a7687089 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java @@ -78,13 +78,13 @@ public interface TimeBar { void setDuration(long duration); /** - * Sets the times of ad breaks. + * Sets the times of ad groups. * - * @param adBreakTimesMs An array where the first {@code adBreakCount} elements are the times of - * ad breaks in milliseconds. May be {@code null} if there are no ad breaks. - * @param adBreakCount The number of ad breaks. + * @param adGroupTimesMs An array where the first {@code adGroupCount} elements are the times of + * ad groups in milliseconds. May be {@code null} if there are no ad groups. + * @param adGroupCount The number of ad groups. */ - void setAdGroupTimesMs(@Nullable long[] adBreakTimesMs, int adBreakCount); + void setAdGroupTimesMs(@Nullable long[] adGroupTimesMs, int adGroupCount); /** * Listener for scrubbing events. From a98d5bbd0a44930d7e179d228d0e662070f0c50e Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 29 Jun 2017 05:58:39 -0700 Subject: [PATCH 193/220] Do not start rebuffering after playback ended. This is currently happening after toggling the repeat mode. This is line with the rest of the implementation which requires a seek operation to resume playback. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160516449 --- .../android/exoplayer2/ExoPlayerTest.java | 26 +++++-------------- .../exoplayer2/ExoPlayerImplInternal.java | 5 ---- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 7bbb6f9306..8d76e8793f 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -226,22 +226,22 @@ public final class ExoPlayerTest extends TestCase { final int[] actionSchedule = { // 0 -> 1 ExoPlayer.REPEAT_MODE_ONE, // 1 -> 1 ExoPlayer.REPEAT_MODE_OFF, // 1 -> 2 - -1, // 2 -> ended - ExoPlayer.REPEAT_MODE_ONE, // ended -> 2 + ExoPlayer.REPEAT_MODE_ONE, // 2 -> 2 ExoPlayer.REPEAT_MODE_ALL, // 2 -> 0 ExoPlayer.REPEAT_MODE_ONE, // 0 -> 0 -1, // 0 -> 0 ExoPlayer.REPEAT_MODE_OFF, // 0 -> 1 -1, // 1 -> 2 - -1, // 2 -> ended - -1 + -1 // 2 -> ended }; - int[] expectedWindowIndices = {1, 1, 2, 2, 2, 0, 0, 0, 1, 2, 2}; + int[] expectedWindowIndices = {1, 1, 2, 2, 0, 0, 0, 1, 2}; final LinkedList windowIndices = new LinkedList<>(); final CountDownLatch actionCounter = new CountDownLatch(actionSchedule.length); PlayerWrapper playerWrapper = new PlayerWrapper() { + @Override @SuppressWarnings("ResourceType") - private void executeAction() { + public void onPositionDiscontinuity() { + super.onPositionDiscontinuity(); int actionIndex = actionSchedule.length - (int) actionCounter.getCount(); if (actionSchedule[actionIndex] != -1) { player.setRepeatMode(actionSchedule[actionIndex]); @@ -249,20 +249,6 @@ public final class ExoPlayerTest extends TestCase { windowIndices.add(player.getCurrentWindowIndex()); actionCounter.countDown(); } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - super.onPlayerStateChanged(playWhenReady, playbackState); - if (playbackState == ExoPlayer.STATE_ENDED) { - executeAction(); - } - } - - @Override - public void onPositionDiscontinuity() { - super.onPositionDiscontinuity(); - executeAction(); - } }; MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT); FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT); 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 b6c9ef6f5d..6f54d5f9e1 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 @@ -488,11 +488,6 @@ import java.io.IOException; long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.positionUs); playbackInfo = new PlaybackInfo(periodId, newPositionUs); } - - // Restart buffering if playback has ended and repetition is enabled. - if (state == ExoPlayer.STATE_ENDED && repeatMode != ExoPlayer.REPEAT_MODE_OFF) { - setState(ExoPlayer.STATE_BUFFERING); - } } private void startRenderers() throws ExoPlaybackException { From 69db6cb60bde95cbd7f86699f631dcbdc71b1ec9 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 29 Jun 2017 06:01:49 -0700 Subject: [PATCH 194/220] Add dynamic concatenating media source. (GitHub issue #1706) The media source allows adding or removing child sources before and after prepare() was called. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160516636 --- .../android/exoplayer2/TimelineTest.java | 32 +- .../DynamicConcatenatingMediaSourceTest.java | 532 ++++++++++++++++ .../DynamicConcatenatingMediaSource.java | 587 ++++++++++++++++++ 3 files changed, 1144 insertions(+), 7 deletions(-) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java index 8b0504253b..699c620da9 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java @@ -63,7 +63,7 @@ public class TimelineTest extends TestCase { @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { - return period.set(new int[] { id, periodIndex }, null, 0, WINDOW_DURATION_US, 0); + return period.set(periodIndex, null, 0, WINDOW_DURATION_US, 0); } @Override @@ -148,12 +148,33 @@ public class TimelineTest extends TestCase { this.timeline = timeline; } - public TimelineVerifier assertWindowIds(int... expectedWindowIds) { + public TimelineVerifier assertEmpty() { + assertWindowIds(); + assertPeriodCounts(); + return this; + } + + /** + * @param expectedWindowIds A list of expected window IDs. If an ID is unknown or not important + * {@code null} can be passed to skip this window. + */ + public TimelineVerifier assertWindowIds(Object... expectedWindowIds) { Window window = new Window(); assertEquals(expectedWindowIds.length, timeline.getWindowCount()); for (int i = 0; i < timeline.getWindowCount(); i++) { timeline.getWindow(i, window, true); - assertEquals(expectedWindowIds[i], window.id); + if (expectedWindowIds[i] != null) { + assertEquals(expectedWindowIds[i], window.id); + } + } + return this; + } + + public TimelineVerifier assertWindowIsDynamic(boolean... windowIsDynamic) { + Window window = new Window(); + for (int i = 0; i < timeline.getWindowCount(); i++) { + timeline.getWindow(i, window, true); + assertEquals(windowIsDynamic[i], window.isDynamic); } return this; } @@ -199,7 +220,6 @@ public class TimelineTest extends TestCase { expectedWindowIndex++; } assertEquals(expectedWindowIndex, period.windowIndex); - assertEquals(i - accumulatedPeriodCounts[expectedWindowIndex], ((int[]) period.id)[1]); if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) { assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, ExoPlayer.REPEAT_MODE_OFF)); @@ -233,9 +253,7 @@ public class TimelineTest extends TestCase { } public void testEmptyTimeline() { - new TimelineVerifier(Timeline.EMPTY) - .assertWindowIds() - .assertPeriodCounts(); + new TimelineVerifier(Timeline.EMPTY).assertEmpty(); } public void testSinglePeriodTimeline() { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java new file mode 100644 index 0000000000..0f63201964 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -0,0 +1,532 @@ +/* + * 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.source; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.TimelineTest; +import com.google.android.exoplayer2.TimelineTest.FakeTimeline; +import com.google.android.exoplayer2.TimelineTest.StubMediaSource; +import com.google.android.exoplayer2.TimelineTest.TimelineVerifier; +import com.google.android.exoplayer2.source.MediaSource.Listener; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.upstream.Allocator; +import java.io.IOException; +import java.util.Arrays; +import junit.framework.TestCase; + +/** + * Unit tests for {@link DynamicConcatenatingMediaSource} + */ +public final class DynamicConcatenatingMediaSourceTest extends TestCase { + + private static final int TIMEOUT_MS = 10000; + + private Timeline timeline; + private boolean timelineUpdated; + + public void testPlaylistChangesAfterPreparation() throws InterruptedException { + timeline = null; + TimelineTest.StubMediaSource[] childSources = createMediaSources(7); + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + prepareAndListenToTimelineUpdates(mediaSource); + waitForTimelineUpdate(); + new TimelineVerifier(timeline).assertEmpty(); + + // Add first source. + mediaSource.addMediaSource(childSources[0]); + waitForTimelineUpdate(); + assertNotNull(timeline); + new TimelineVerifier(timeline) + .assertPeriodCounts(1) + .assertWindowIds(111); + + // Add at front of queue. + mediaSource.addMediaSource(0, childSources[1]); + waitForTimelineUpdate(); + new TimelineVerifier(timeline) + .assertPeriodCounts(2, 1) + .assertWindowIds(222, 111); + + // Add at back of queue. + mediaSource.addMediaSource(childSources[2]); + waitForTimelineUpdate(); + new TimelineVerifier(timeline) + .assertPeriodCounts(2, 1, 3) + .assertWindowIds(222, 111, 333); + + // Add in the middle. + mediaSource.addMediaSource(1, childSources[3]); + waitForTimelineUpdate(); + new TimelineVerifier(timeline) + .assertPeriodCounts(2, 4, 1, 3) + .assertWindowIds(222, 444, 111, 333); + + // Add bulk. + mediaSource.addMediaSources(3, Arrays.asList((MediaSource) childSources[4], + (MediaSource) childSources[5], (MediaSource) childSources[6])); + waitForTimelineUpdate(); + new TimelineVerifier(timeline) + .assertPeriodCounts(2, 4, 1, 5, 6, 7, 3) + .assertWindowIds(222, 444, 111, 555, 666, 777, 333); + + // Remove in the middle. + mediaSource.removeMediaSource(3); + waitForTimelineUpdate(); + mediaSource.removeMediaSource(3); + waitForTimelineUpdate(); + mediaSource.removeMediaSource(3); + waitForTimelineUpdate(); + mediaSource.removeMediaSource(1); + waitForTimelineUpdate(); + new TimelineVerifier(timeline) + .assertPeriodCounts(2, 1, 3) + .assertWindowIds(222, 111, 333); + for (int i = 3; i <= 6; i++) { + childSources[i].assertReleased(); + } + + // Remove at front of queue. + mediaSource.removeMediaSource(0); + waitForTimelineUpdate(); + new TimelineVerifier(timeline) + .assertPeriodCounts(1, 3) + .assertWindowIds(111, 333); + childSources[1].assertReleased(); + + // Remove at back of queue. + mediaSource.removeMediaSource(1); + waitForTimelineUpdate(); + new TimelineVerifier(timeline) + .assertPeriodCounts(1) + .assertWindowIds(111); + childSources[2].assertReleased(); + + // Remove last source. + mediaSource.removeMediaSource(0); + waitForTimelineUpdate(); + new TimelineVerifier(timeline) + .assertPeriodCounts() + .assertWindowIds(); + childSources[3].assertReleased(); + } + + public void testPlaylistChangesBeforePreparation() throws InterruptedException { + timeline = null; + TimelineTest.StubMediaSource[] childSources = createMediaSources(4); + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + mediaSource.addMediaSource(childSources[0]); + mediaSource.addMediaSource(childSources[1]); + mediaSource.addMediaSource(0, childSources[2]); + mediaSource.removeMediaSource(1); + mediaSource.addMediaSource(1, childSources[3]); + assertNull(timeline); + + prepareAndListenToTimelineUpdates(mediaSource); + waitForTimelineUpdate(); + assertNotNull(timeline); + new TimelineVerifier(timeline) + .assertPeriodCounts(3, 4, 2) + .assertWindowIds(333, 444, 222); + + mediaSource.releaseSource(); + for (int i = 1; i < 4; i++) { + childSources[i].assertReleased(); + } + } + + public void testPlaylistWithLazyMediaSource() throws InterruptedException { + timeline = null; + TimelineTest.StubMediaSource[] childSources = createMediaSources(2); + LazyMediaSource[] lazySources = new LazyMediaSource[4]; + for (int i = 0; i < 4; i++) { + lazySources[i] = new LazyMediaSource(); + } + + //Add lazy sources before preparation + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + mediaSource.addMediaSource(lazySources[0]); + mediaSource.addMediaSource(0, childSources[0]); + mediaSource.removeMediaSource(1); + mediaSource.addMediaSource(1, lazySources[1]); + assertNull(timeline); + prepareAndListenToTimelineUpdates(mediaSource); + waitForTimelineUpdate(); + assertNotNull(timeline); + new TimelineVerifier(timeline) + .assertPeriodCounts(1, 1) + .assertWindowIds(111, null) + .assertWindowIsDynamic(false, true); + + lazySources[1].triggerTimelineUpdate(new FakeTimeline(9, 999)); + waitForTimelineUpdate(); + new TimelineVerifier(timeline) + .assertPeriodCounts(1, 9) + .assertWindowIds(111, 999) + .assertWindowIsDynamic(false, false); + + //Add lazy sources after preparation + mediaSource.addMediaSource(1, lazySources[2]); + waitForTimelineUpdate(); + mediaSource.addMediaSource(2, childSources[1]); + waitForTimelineUpdate(); + mediaSource.addMediaSource(0, lazySources[3]); + waitForTimelineUpdate(); + mediaSource.removeMediaSource(2); + waitForTimelineUpdate(); + new TimelineVerifier(timeline) + .assertPeriodCounts(1, 1, 2, 9) + .assertWindowIds(null, 111, 222, 999) + .assertWindowIsDynamic(true, false, false, false); + + lazySources[3].triggerTimelineUpdate(new FakeTimeline(8, 888)); + waitForTimelineUpdate(); + new TimelineVerifier(timeline) + .assertPeriodCounts(8, 1, 2, 9) + .assertWindowIds(888, 111, 222, 999) + .assertWindowIsDynamic(false, false, false, false); + + mediaSource.releaseSource(); + childSources[0].assertReleased(); + childSources[1].assertReleased(); + } + + public void testIllegalArguments() { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + MediaSource validSource = new StubMediaSource(new FakeTimeline(1, 1)); + + // Null sources. + try { + mediaSource.addMediaSource(null); + fail("Null mediaSource not allowed."); + } catch (NullPointerException e) { + // Expected. + } + + MediaSource[] mediaSources = { validSource, null }; + try { + mediaSource.addMediaSources(Arrays.asList(mediaSources)); + fail("Null mediaSource not allowed."); + } catch (NullPointerException e) { + // Expected. + } + + // Duplicate sources. + mediaSource.addMediaSource(validSource); + try { + mediaSource.addMediaSource(validSource); + fail("Duplicate mediaSource not allowed."); + } catch (IllegalArgumentException e) { + // Expected. + } + + mediaSources = new MediaSource[] { new StubMediaSource(new FakeTimeline(1, 1)), validSource}; + try { + mediaSource.addMediaSources(Arrays.asList(mediaSources)); + fail("Duplicate mediaSource not allowed."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + private void prepareAndListenToTimelineUpdates(MediaSource mediaSource) { + mediaSource.prepareSource(new StubExoPlayer(), true, new Listener() { + @Override + public void onSourceInfoRefreshed(Timeline newTimeline, Object manifest) { + timeline = newTimeline; + synchronized (DynamicConcatenatingMediaSourceTest.this) { + timelineUpdated = true; + DynamicConcatenatingMediaSourceTest.this.notify(); + } + } + }); + } + + private synchronized void waitForTimelineUpdate() throws InterruptedException { + long timeoutMs = System.currentTimeMillis() + TIMEOUT_MS; + while (!timelineUpdated) { + wait(TIMEOUT_MS); + if (System.currentTimeMillis() >= timeoutMs) { + fail("No timeline update occurred within timeout."); + } + } + timelineUpdated = false; + } + + private TimelineTest.StubMediaSource[] createMediaSources(int count) { + TimelineTest.StubMediaSource[] sources = new TimelineTest.StubMediaSource[count]; + for (int i = 0; i < count; i++) { + sources[i] = new TimelineTest.StubMediaSource(new FakeTimeline(i + 1, (i + 1) * 111)); + } + return sources; + } + + private static class LazyMediaSource implements MediaSource { + + private Listener listener; + + public void triggerTimelineUpdate(Timeline timeline) { + listener.onSourceInfoRefreshed(timeline, null); + } + + @Override + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + this.listener = listener; + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + return null; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + } + + @Override + public void releaseSource() { + } + + } + + /** + * Stub ExoPlayer which only accepts custom messages and runs them on a separate handler thread. + */ + private static class StubExoPlayer implements ExoPlayer, Handler.Callback { + + private final Handler handler; + + public StubExoPlayer() { + HandlerThread handlerThread = new HandlerThread("StubExoPlayerThread"); + handlerThread.start(); + handler = new Handler(handlerThread.getLooper(), this); + } + + @Override + public Looper getPlaybackLooper() { + throw new UnsupportedOperationException(); + } + + @Override + public void addListener(EventListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeListener(EventListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public int getPlaybackState() { + throw new UnsupportedOperationException(); + } + + @Override + public void prepare(MediaSource mediaSource) { + throw new UnsupportedOperationException(); + } + + @Override + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPlayWhenReady(boolean playWhenReady) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getPlayWhenReady() { + throw new UnsupportedOperationException(); + } + + @Override + public void setRepeatMode(@RepeatMode int repeatMode) { + throw new UnsupportedOperationException(); + } + + @Override + public int getRepeatMode() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isLoading() { + throw new UnsupportedOperationException(); + } + + @Override + public void seekToDefaultPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public void seekToDefaultPosition(int windowIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public void seekTo(long positionMs) { + throw new UnsupportedOperationException(); + } + + @Override + public void seekTo(int windowIndex, long positionMs) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + throw new UnsupportedOperationException(); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + throw new UnsupportedOperationException(); + } + + @Override + public void stop() { + throw new UnsupportedOperationException(); + } + + @Override + public void release() { + throw new UnsupportedOperationException(); + } + + @Override + public void sendMessages(ExoPlayerMessage... messages) { + handler.obtainMessage(0, messages).sendToTarget(); + } + + @Override + public void blockingSendMessages(ExoPlayerMessage... messages) { + throw new UnsupportedOperationException(); + } + + @Override + public int getRendererCount() { + throw new UnsupportedOperationException(); + } + + @Override + public int getRendererType(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public TrackGroupArray getCurrentTrackGroups() { + throw new UnsupportedOperationException(); + } + + @Override + public TrackSelectionArray getCurrentTrackSelections() { + throw new UnsupportedOperationException(); + } + + @Override + public Object getCurrentManifest() { + throw new UnsupportedOperationException(); + } + + @Override + public Timeline getCurrentTimeline() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentPeriodIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentWindowIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public long getDuration() { + throw new UnsupportedOperationException(); + } + + @Override + public long getCurrentPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public long getBufferedPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public int getBufferedPercentage() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCurrentWindowDynamic() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCurrentWindowSeekable() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isPlayingAd() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentAdGroupIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentAdIndexInAdGroup() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean handleMessage(Message msg) { + ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; + for (ExoPlayerMessage message : messages) { + try { + message.target.handleMessage(message.messageType, message.message); + } catch (ExoPlaybackException e) { + fail("Unexpected ExoPlaybackException."); + } + } + return true; + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java new file mode 100644 index 0000000000..a9e478a67f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -0,0 +1,587 @@ +/* + * 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.source; + +import android.util.Pair; +import android.util.SparseIntArray; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; +import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +/** + * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified + * during playback. Access to this class is thread-safe. + */ +public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPlayerComponent { + + private static final int MSG_ADD = 0; + private static final int MSG_ADD_MULTIPLE = 1; + private static final int MSG_REMOVE = 2; + + // Accessed on the app thread. + private final List mediaSourcesPublic; + + // Accessed on the playback thread. + private final List mediaSourceHolders; + private final MediaSourceHolder query; + private final Map mediaSourceByMediaPeriod; + private final List deferredMediaPeriods; + + private ExoPlayer player; + private Listener listener; + private boolean preventListenerNotification; + private int windowCount; + private int periodCount; + + public DynamicConcatenatingMediaSource() { + this.mediaSourceByMediaPeriod = new IdentityHashMap<>(); + this.mediaSourcesPublic = new ArrayList<>(); + this.mediaSourceHolders = new ArrayList<>(); + this.deferredMediaPeriods = new ArrayList<>(1); + this.query = new MediaSourceHolder(null, null, -1, -1, -1); + } + + /** + * Appends a {@link MediaSource} to the playlist. + * + * @param mediaSource The {@link MediaSource} to be added to the list. + */ + public synchronized void addMediaSource(MediaSource mediaSource) { + addMediaSource(mediaSourcesPublic.size(), mediaSource); + } + + /** + * Adds a {@link MediaSource} to the playlist. + * + * @param index The index at which the new {@link MediaSource} will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @param mediaSource The {@link MediaSource} to be added to the list. + */ + public synchronized void addMediaSource(int index, MediaSource mediaSource) { + Assertions.checkNotNull(mediaSource); + Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); + mediaSourcesPublic.add(index, mediaSource); + if (player != null) { + player.sendMessages(new ExoPlayerMessage(this, MSG_ADD, Pair.create(index, mediaSource))); + } + } + + /** + * Appends multiple {@link MediaSource}s to the playlist. + * + * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media + * sources are added in the order in which they appear in this collection. + */ + public synchronized void addMediaSources(Collection mediaSources) { + addMediaSources(mediaSourcesPublic.size(), mediaSources); + } + + /** + * Adds multiple {@link MediaSource}s to the playlist. + * + * @param index The index at which the new {@link MediaSource}s will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media + * sources are added in the order in which they appear in this collection. + */ + public synchronized void addMediaSources(int index, Collection mediaSources) { + for (MediaSource mediaSource : mediaSources) { + Assertions.checkNotNull(mediaSource); + Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); + } + mediaSourcesPublic.addAll(index, mediaSources); + if (player != null && !mediaSources.isEmpty()) { + player.sendMessages(new ExoPlayerMessage(this, MSG_ADD_MULTIPLE, + Pair.create(index, mediaSources))); + } + } + + /** + * Removes a {@link MediaSource} from the playlist. + * + * @param index The index at which the media source will be removed. + */ + public synchronized void removeMediaSource(int index) { + mediaSourcesPublic.remove(index); + if (player != null) { + player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, index)); + } + } + + /** + * Returns the number of media sources in the playlist. + */ + public synchronized int getSize() { + return mediaSourcesPublic.size(); + } + + /** + * Returns the {@link MediaSource} at a specified index. + * + * @param index A index in the range of 0 <= index <= {@link #getSize()}. + * @return The {@link MediaSource} at this index. + */ + public synchronized MediaSource getMediaSource(int index) { + return mediaSourcesPublic.get(index); + } + + @Override + public synchronized void prepareSource(ExoPlayer player, boolean isTopLevelSource, + Listener listener) { + this.player = player; + this.listener = listener; + preventListenerNotification = true; + addMediaSourcesInternal(0, mediaSourcesPublic); + preventListenerNotification = false; + maybeNotifyListener(); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { + mediaSourceHolder.mediaSource.maybeThrowSourceInfoRefreshError(); + } + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + int mediaSourceHolderIndex = findMediaSourceHolderByPeriodIndex(id.periodIndex); + MediaSourceHolder holder = mediaSourceHolders.get(mediaSourceHolderIndex); + MediaPeriodId idInSource = new MediaPeriodId(id.periodIndex - holder.firstPeriodIndexInChild); + MediaPeriod mediaPeriod; + if (!holder.isPrepared) { + mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, idInSource, allocator); + deferredMediaPeriods.add((DeferredMediaPeriod) mediaPeriod); + } else { + mediaPeriod = holder.mediaSource.createPeriod(idInSource, allocator); + } + mediaSourceByMediaPeriod.put(mediaPeriod, holder.mediaSource); + return mediaPeriod; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + MediaSource mediaSource = mediaSourceByMediaPeriod.get(mediaPeriod); + mediaSourceByMediaPeriod.remove(mediaPeriod); + if (mediaPeriod instanceof DeferredMediaPeriod) { + deferredMediaPeriods.remove(mediaPeriod); + ((DeferredMediaPeriod) mediaPeriod).releasePeriod(); + } else { + mediaSource.releasePeriod(mediaPeriod); + } + } + + @Override + public void releaseSource() { + for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { + mediaSourceHolder.mediaSource.releaseSource(); + } + } + + @Override + @SuppressWarnings("unchecked") + public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + preventListenerNotification = true; + switch (messageType) { + case MSG_ADD: { + Pair messageData = (Pair) message; + addMediaSourceInternal(messageData.first, messageData.second); + break; + } + case MSG_ADD_MULTIPLE: { + Pair> messageData = + (Pair>) message; + addMediaSourcesInternal(messageData.first, messageData.second); + break; + } + case MSG_REMOVE: { + removeMediaSourceInternal((Integer) message); + break; + } + default: { + throw new IllegalStateException(); + } + } + preventListenerNotification = false; + maybeNotifyListener(); + } + + private void maybeNotifyListener() { + if (!preventListenerNotification) { + listener.onSourceInfoRefreshed( + new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount), null); + } + } + + private void addMediaSourceInternal(int newIndex, MediaSource newMediaSource) { + final MediaSourceHolder newMediaSourceHolder; + Object newUid = System.identityHashCode(newMediaSource); + DeferredTimeline newTimeline = new DeferredTimeline(); + if (newIndex > 0) { + MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1); + newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline, + previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount(), + previousHolder.firstPeriodIndexInChild + previousHolder.timeline.getPeriodCount(), + newUid); + } else { + newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline, 0, 0, newUid); + } + correctOffsets(newIndex, newTimeline.getWindowCount(), newTimeline.getPeriodCount()); + mediaSourceHolders.add(newIndex, newMediaSourceHolder); + newMediaSourceHolder.mediaSource.prepareSource(player, false, new Listener() { + @Override + public void onSourceInfoRefreshed(Timeline newTimeline, Object manifest) { + updateMediaSourceInternal(newMediaSourceHolder, newTimeline); + } + }); + } + + private void addMediaSourcesInternal(int index, Collection mediaSources) { + for (MediaSource mediaSource : mediaSources) { + addMediaSourceInternal(index++, mediaSource); + } + } + + private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) { + if (mediaSourceHolder == null) { + throw new IllegalArgumentException(); + } + DeferredTimeline deferredTimeline = mediaSourceHolder.timeline; + if (deferredTimeline.getTimeline() == timeline) { + return; + } + int windowOffsetUpdate = timeline.getWindowCount() - deferredTimeline.getWindowCount(); + int periodOffsetUpdate = timeline.getPeriodCount() - deferredTimeline.getPeriodCount(); + if (windowOffsetUpdate != 0 || periodOffsetUpdate != 0) { + int index = findMediaSourceHolderByPeriodIndex(mediaSourceHolder.firstPeriodIndexInChild); + correctOffsets(index + 1, windowOffsetUpdate, periodOffsetUpdate); + } + mediaSourceHolder.timeline = deferredTimeline.cloneWithNewTimeline(timeline); + if (!mediaSourceHolder.isPrepared) { + for (int i = deferredMediaPeriods.size() - 1; i >= 0; i--) { + if (deferredMediaPeriods.get(i).mediaSource == mediaSourceHolder.mediaSource) { + deferredMediaPeriods.get(i).createPeriod(); + deferredMediaPeriods.remove(i); + } + } + } + mediaSourceHolder.isPrepared = true; + maybeNotifyListener(); + } + + private void removeMediaSourceInternal(int index) { + MediaSourceHolder holder = mediaSourceHolders.get(index); + mediaSourceHolders.remove(index); + Timeline oldTimeline = holder.timeline; + correctOffsets(index, -oldTimeline.getWindowCount(), -oldTimeline.getPeriodCount()); + holder.mediaSource.releaseSource(); + } + + private void correctOffsets(int startIndex, int windowOffsetUpdate, int periodOffsetUpdate) { + windowCount += windowOffsetUpdate; + periodCount += periodOffsetUpdate; + for (int i = startIndex; i < mediaSourceHolders.size(); i++) { + mediaSourceHolders.get(i).firstWindowIndexInChild += windowOffsetUpdate; + mediaSourceHolders.get(i).firstPeriodIndexInChild += periodOffsetUpdate; + } + } + + private int findMediaSourceHolderByPeriodIndex(int periodIndex) { + query.firstPeriodIndexInChild = periodIndex; + int index = Collections.binarySearch(mediaSourceHolders, query); + return index >= 0 ? index : -index - 2; + } + + private static final class MediaSourceHolder implements Comparable { + + public final MediaSource mediaSource; + public final Object uid; + + public DeferredTimeline timeline; + public int firstWindowIndexInChild; + public int firstPeriodIndexInChild; + public boolean isPrepared; + + public MediaSourceHolder(MediaSource mediaSource, DeferredTimeline timeline, int window, + int period, Object uid) { + this.mediaSource = mediaSource; + this.timeline = timeline; + this.firstWindowIndexInChild = window; + this.firstPeriodIndexInChild = period; + this.uid = uid; + } + + @Override + public int compareTo(MediaSourceHolder other) { + return this.firstPeriodIndexInChild - other.firstPeriodIndexInChild; + } + } + + private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline { + + private final int windowCount; + private final int periodCount; + private final int[] firstPeriodInChildIndices; + private final int[] firstWindowInChildIndices; + private final Timeline[] timelines; + private final int[] uids; + private final SparseIntArray childIndexByUid; + + public ConcatenatedTimeline(Collection mediaSourceHolders, int windowCount, + int periodCount) { + this.windowCount = windowCount; + this.periodCount = periodCount; + int childCount = mediaSourceHolders.size(); + firstPeriodInChildIndices = new int[childCount]; + firstWindowInChildIndices = new int[childCount]; + timelines = new Timeline[childCount]; + uids = new int[childCount]; + childIndexByUid = new SparseIntArray(); + int index = 0; + for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { + timelines[index] = mediaSourceHolder.timeline; + firstPeriodInChildIndices[index] = mediaSourceHolder.firstPeriodIndexInChild; + firstWindowInChildIndices[index] = mediaSourceHolder.firstWindowIndexInChild; + uids[index] = (int) mediaSourceHolder.uid; + childIndexByUid.put(uids[index], index++); + } + } + + @Override + protected void getChildDataByPeriodIndex(int periodIndex, ChildDataHolder childDataHolder) { + int index = Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex, true, false); + setChildData(index, childDataHolder); + } + + @Override + protected void getChildDataByWindowIndex(int windowIndex, ChildDataHolder childDataHolder) { + int index = Util.binarySearchFloor(firstWindowInChildIndices, windowIndex, true, false); + setChildData(index, childDataHolder); + } + + @Override + protected boolean getChildDataByChildUid(Object childUid, ChildDataHolder childDataHolder) { + if (!(childUid instanceof Integer)) { + return false; + } + int index = childIndexByUid.get((int) childUid, -1); + if (index == -1) { + return false; + } + setChildData(index, childDataHolder); + return true; + } + + @Override + public int getWindowCount() { + return windowCount; + } + + @Override + public int getPeriodCount() { + return periodCount; + } + + private void setChildData(int srcIndex, ChildDataHolder dest) { + dest.setData(timelines[srcIndex], firstPeriodInChildIndices[srcIndex], + firstWindowInChildIndices[srcIndex], uids[srcIndex]); + } + } + + private static final class DeferredTimeline extends Timeline { + + private static final Object DUMMY_ID = new Object(); + private static final Period period = new Period(); + + private final Timeline timeline; + private final Object replacedID; + + public DeferredTimeline() { + timeline = null; + replacedID = null; + } + + private DeferredTimeline(Timeline timeline, Object replacedID) { + this.timeline = timeline; + this.replacedID = replacedID; + } + + public DeferredTimeline cloneWithNewTimeline(Timeline timeline) { + return new DeferredTimeline(timeline, replacedID == null && timeline.getPeriodCount() > 0 + ? timeline.getPeriod(0, period, true).uid : replacedID); + } + + public Timeline getTimeline() { + return timeline; + } + + @Override + public int getWindowCount() { + return timeline == null ? 1 : timeline.getWindowCount(); + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + return timeline == null + // Dynamic window to indicate pending timeline updates. + ? window.set(setIds ? DUMMY_ID : null, C.TIME_UNSET, C.TIME_UNSET, false, true, 0, + C.TIME_UNSET, 0, 0, 0) + : timeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); + } + + @Override + public int getPeriodCount() { + return timeline == null ? 1 : timeline.getPeriodCount(); + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + if (timeline == null) { + return period.set(setIds ? DUMMY_ID : null, setIds ? DUMMY_ID : null, 0, C.TIME_UNSET, + C.TIME_UNSET); + } + timeline.getPeriod(periodIndex, period, setIds); + if (period.uid == replacedID) { + period.uid = DUMMY_ID; + } + return period; + } + + @Override + public int getIndexOfPeriod(Object uid) { + return timeline == null ? (uid == DUMMY_ID ? 0 : C.INDEX_UNSET) + : timeline.getIndexOfPeriod(uid == DUMMY_ID ? replacedID : uid); + } + + } + + private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { + + public final MediaSource mediaSource; + + private final MediaPeriodId id; + private final Allocator allocator; + + private MediaPeriod mediaPeriod; + private Callback callback; + private long preparePositionUs; + + public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) { + this.id = id; + this.allocator = allocator; + this.mediaSource = mediaSource; + } + + public void createPeriod() { + mediaPeriod = mediaSource.createPeriod(id, allocator); + if (callback != null) { + mediaPeriod.prepare(this, preparePositionUs); + } + } + + public void releasePeriod() { + if (mediaPeriod != null) { + mediaSource.releasePeriod(mediaPeriod); + } + } + + @Override + public void prepare(Callback callback, long preparePositionUs) { + this.callback = callback; + this.preparePositionUs = preparePositionUs; + if (mediaPeriod != null) { + mediaPeriod.prepare(this, preparePositionUs); + } + } + + @Override + public void maybeThrowPrepareError() throws IOException { + if (mediaPeriod != null) { + mediaPeriod.maybeThrowPrepareError(); + } else { + mediaSource.maybeThrowSourceInfoRefreshError(); + } + } + + @Override + public TrackGroupArray getTrackGroups() { + return mediaPeriod.getTrackGroups(); + } + + @Override + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, + positionUs); + } + + @Override + public void discardBuffer(long positionUs) { + mediaPeriod.discardBuffer(positionUs); + } + + @Override + public long readDiscontinuity() { + return mediaPeriod.readDiscontinuity(); + } + + @Override + public long getBufferedPositionUs() { + return mediaPeriod.getBufferedPositionUs(); + } + + @Override + public long seekToUs(long positionUs) { + return mediaPeriod.seekToUs(positionUs); + } + + @Override + public long getNextLoadPositionUs() { + return mediaPeriod.getNextLoadPositionUs(); + } + + @Override + public boolean continueLoading(long positionUs) { + return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + callback.onContinueLoadingRequested(this); + } + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + callback.onPrepared(this); + } + } + +} From 51af85f263a34bce3ae518f1d457dcf540929750 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 29 Jun 2017 06:49:30 -0700 Subject: [PATCH 195/220] Prefer Google over MediaTek for PCM decoding pre-O. Issue: #2873 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160520136 --- .../exoplayer2/mediacodec/MediaCodecUtil.java | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) 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 73ceff2754..d3f3dae344 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 @@ -56,8 +56,10 @@ public final class MediaCodecUtil { } private static final String TAG = "MediaCodecUtil"; + private static final String GOOGLE_RAW_DECODER_NAME = "OMX.google.raw.decoder"; + private static final String MTK_RAW_DECODER_NAME = "OMX.MTK.AUDIO.DECODER.RAW"; private static final MediaCodecInfo PASSTHROUGH_DECODER_INFO = - MediaCodecInfo.newPassthroughInstance("OMX.google.raw.decoder"); + MediaCodecInfo.newPassthroughInstance(GOOGLE_RAW_DECODER_NAME); private static final Pattern PROFILE_PATTERN = Pattern.compile("^\\D?(\\d+)$"); private static final HashMap> decoderInfosCache = new HashMap<>(); @@ -155,6 +157,7 @@ public final class MediaCodecUtil { + ". Assuming: " + decoderInfos.get(0).name); } } + applyWorkarounds(decoderInfos); decoderInfos = Collections.unmodifiableList(decoderInfos); decoderInfosCache.put(key, decoderInfos); return decoderInfos; @@ -339,6 +342,27 @@ public final class MediaCodecUtil { return true; } + /** + * Modifies a list of {@link MediaCodecInfo}s to apply workarounds where we know better than the + * platform. + * + * @param decoderInfos The list to modify. + */ + private static void applyWorkarounds(List decoderInfos) { + if (Util.SDK_INT < 26 && decoderInfos.size() > 1 + && MTK_RAW_DECODER_NAME.equals(decoderInfos.get(0).name)) { + // Prefer the Google raw decoder over the MediaTek one [Internal: b/62337687]. + for (int i = 1; i < decoderInfos.size(); i++) { + MediaCodecInfo decoderInfo = decoderInfos.get(i); + if (GOOGLE_RAW_DECODER_NAME.equals(decoderInfo.name)) { + decoderInfos.remove(i); + decoderInfos.add(0, decoderInfo); + break; + } + } + } + } + /** * Returns whether the decoder is known to fail when adapting, despite advertising itself as an * adaptive decoder. From e344e9cbb756a384c437850028c3fb09e145a386 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 29 Jun 2017 06:53:56 -0700 Subject: [PATCH 196/220] Add SinglePeriodTimeline constructor to fill missing window information Issue:#2930 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160520480 --- .../source/SinglePeriodTimeline.java | 33 +++++++++++++++++-- .../exoplayer2/source/hls/HlsMediaSource.java | 13 +++++--- 2 files changed, 39 insertions(+), 7 deletions(-) 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 ae367ef14c..6f35438444 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 @@ -26,6 +26,8 @@ public final class SinglePeriodTimeline extends Timeline { private static final Object ID = new Object(); + private final long presentationStartTimeMs; + private final long windowStartTimeMs; private final long periodDurationUs; private final long windowDurationUs; private final long windowPositionInPeriodUs; @@ -45,8 +47,8 @@ public final class SinglePeriodTimeline extends Timeline { } /** - * Creates a timeline with one period of known duration, and a window of known duration starting - * at a specified position in the period. + * Creates a timeline with one period, and a window of known duration starting at a specified + * position in the period. * * @param periodDurationUs The duration of the period in microseconds. * @param windowDurationUs The duration of the window in microseconds. @@ -60,6 +62,31 @@ public final class SinglePeriodTimeline extends Timeline { public SinglePeriodTimeline(long periodDurationUs, long windowDurationUs, long windowPositionInPeriodUs, long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic) { + this(C.TIME_UNSET, C.TIME_UNSET, periodDurationUs, windowDurationUs, windowPositionInPeriodUs, + windowDefaultStartPositionUs, isSeekable, isDynamic); + } + + /** + * Creates a timeline with one period, and a window of known duration starting at a specified + * position in the period. + * + * @param presentationStartTimeMs The start time of the presentation in milliseconds since the + * epoch. + * @param windowStartTimeMs The window's start time in milliseconds since the epoch. + * @param periodDurationUs The duration of the period in microseconds. + * @param windowDurationUs The duration of the window in microseconds. + * @param windowPositionInPeriodUs The position of the start of the window in the period, in + * microseconds. + * @param windowDefaultStartPositionUs The default position relative to the start of the window at + * 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. + */ + public SinglePeriodTimeline(long presentationStartTimeMs, long windowStartTimeMs, + long periodDurationUs, long windowDurationUs, long windowPositionInPeriodUs, + long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic) { + this.presentationStartTimeMs = presentationStartTimeMs; + this.windowStartTimeMs = windowStartTimeMs; this.periodDurationUs = periodDurationUs; this.windowDurationUs = windowDurationUs; this.windowPositionInPeriodUs = windowPositionInPeriodUs; @@ -86,7 +113,7 @@ public final class SinglePeriodTimeline extends Timeline { windowDefaultStartPositionUs = C.TIME_UNSET; } } - return window.set(id, C.TIME_UNSET, C.TIME_UNSET, isSeekable, isDynamic, + return window.set(id, presentationStartTimeMs, windowStartTimeMs, isSeekable, isDynamic, windowDefaultStartPositionUs, windowDurationUs, 0, 0, windowPositionInPeriodUs); } 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 0c97fb5ecc..aa09c16ca7 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 @@ -111,6 +111,9 @@ public final class HlsMediaSource implements MediaSource, @Override public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) { SinglePeriodTimeline timeline; + long presentationStartTimeMs = playlist.hasProgramDateTime ? 0 : C.TIME_UNSET; + long windowStartTimeMs = playlist.hasProgramDateTime ? C.usToMs(playlist.startTimeUs) + : C.TIME_UNSET; long windowDefaultStartPositionUs = playlist.startOffsetUs; if (playlistTracker.isLive()) { long periodDurationUs = playlist.hasEndTag ? (playlist.startTimeUs + playlist.durationUs) @@ -120,14 +123,16 @@ public final class HlsMediaSource implements MediaSource, windowDefaultStartPositionUs = segments.isEmpty() ? 0 : segments.get(Math.max(0, segments.size() - 3)).relativeStartTimeUs; } - timeline = new SinglePeriodTimeline(periodDurationUs, playlist.durationUs, - playlist.startTimeUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag); + timeline = new SinglePeriodTimeline(presentationStartTimeMs, windowStartTimeMs, + periodDurationUs, playlist.durationUs, playlist.startTimeUs, windowDefaultStartPositionUs, + true, !playlist.hasEndTag); } else /* not live */ { if (windowDefaultStartPositionUs == C.TIME_UNSET) { windowDefaultStartPositionUs = 0; } - timeline = new SinglePeriodTimeline(playlist.startTimeUs + playlist.durationUs, - playlist.durationUs, playlist.startTimeUs, windowDefaultStartPositionUs, true, false); + timeline = new SinglePeriodTimeline(presentationStartTimeMs, windowStartTimeMs, + playlist.startTimeUs + playlist.durationUs, playlist.durationUs, playlist.startTimeUs, + windowDefaultStartPositionUs, true, false); } sourceListener.onSourceInfoRefreshed(timeline, new HlsManifest(playlistTracker.getMasterPlaylist(), playlist)); From beb07341070a9ff233d3773afed0e49fb416bf17 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 29 Jun 2017 07:30:24 -0700 Subject: [PATCH 197/220] Extract base class from DashDownloaderFactory This base class will be used to extend HlsDownloaderFactory from. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160523335 --- .../source/offline/DownloaderFactory.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/offline/DownloaderFactory.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/offline/DownloaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/offline/DownloaderFactory.java new file mode 100644 index 0000000000..1bce12ce05 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/offline/DownloaderFactory.java @@ -0,0 +1,88 @@ +/* + * 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.source.offline; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.upstream.DataSink; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSource.Factory; +import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.PriorityTaskManager; + +/** + * A factory class that produces {@link Downloader}. + */ +@ClosedSource(reason = "Not ready yet") +public abstract class DownloaderFactory> { + + private final Factory upstreamDataSourceFactory; + private final Factory cacheReadDataSourceFactory; + private final DataSink.Factory cacheWriteDataSinkFactory; + + protected final Cache cache; + protected final PriorityTaskManager priorityTaskManager; + + /** + * Constructs a DashDownloaderFactory. + * + * @param cache Cache instance to be used to store downloaded data. + * @param upstreamDataSourceFactory A {@link Factory} for downloading data. + */ + public DownloaderFactory(Cache cache, Factory upstreamDataSourceFactory) { + this(cache, upstreamDataSourceFactory, null, null, null); + } + + /** + * Constructs a DashDownloaderFactory. + * + * @param cache Cache instance to be used to store downloaded data. + * @param upstreamDataSourceFactory A {@link Factory} for downloading data. + * @param cacheReadDataSourceFactory A {@link Factory} for reading data from the cache. + * If null, null is passed to {@link Downloader} constructor. + * @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for writing data to the cache. If + * null, null is passed to {@link Downloader} constructor. + * @param priorityTaskManager If one is given then the download priority is set lower than + * loading. If null, null is passed to {@link Downloader} constructor. + */ + public DownloaderFactory(Cache cache, Factory upstreamDataSourceFactory, + @Nullable Factory cacheReadDataSourceFactory, + @Nullable DataSink.Factory cacheWriteDataSinkFactory, + @Nullable PriorityTaskManager priorityTaskManager) { + this.cache = cache; + this.upstreamDataSourceFactory = upstreamDataSourceFactory; + this.cacheReadDataSourceFactory = cacheReadDataSourceFactory; + this.cacheWriteDataSinkFactory = cacheWriteDataSinkFactory; + this.priorityTaskManager = priorityTaskManager; + } + + /** + * Creates a {@link Downloader} with the given manifest. + * + * @param manifestUri The URI of the manifest of the DASH to be downloaded. + * @return A {@link Downloader}. + */ + public final T create(String manifestUri) { + return create(manifestUri, + upstreamDataSourceFactory != null ? upstreamDataSourceFactory.createDataSource() : null, + cacheReadDataSourceFactory != null ? cacheReadDataSourceFactory.createDataSource() : null, + cacheWriteDataSinkFactory != null ? cacheWriteDataSinkFactory.createDataSink() : null); + } + + protected abstract T create(String manifestUri, DataSource upstreamDataSource, + DataSource cacheReadDataSource, DataSink cacheWriteDataSink); + +} From db177db6d9aa11b765a25496630f220bab2edc76 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 29 Jun 2017 09:41:36 -0700 Subject: [PATCH 198/220] DrmSession cleanup These changes are in part related to handling playback of mixed clear and encrypted content, where we might want to use a secure decoder throughout, but only have drm init data and only care about the state of the DrmSession during playback of encrypted parts. - requiresSecureDecoderComponent became unnecessary when we added ExoMediaCrypto, which provides a layer in which requiresSecureDecoderComponent can be overridden. - Relaxed requirements for obtaining the MediaCrypto. It's helpful to allow retrieval in the error state, since it can be used to instantiate a decoder and play clear samples. - Deferred throwing of errors in renderer implementations. As long as we can get a MediaCrypto, we should init the codec. We can also play clear samples without failing if playClearSamplesWithoutKeys is true, regardless of the errors state. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160536365 --- .../ext/vp9/LibvpxVideoRenderer.java | 87 +++++++++++-------- .../android/exoplayer2/audio/AudioTrack.java | 2 +- .../audio/SimpleDecoderAudioRenderer.java | 19 ++-- .../drm/DefaultDrmSessionManager.java | 31 ++----- .../android/exoplayer2/drm/DrmSession.java | 72 +++++---------- .../exoplayer2/drm/FrameworkMediaCrypto.java | 8 +- .../exoplayer2/drm/FrameworkMediaDrm.java | 9 +- .../android/exoplayer2/drm/WidevineUtil.java | 12 +-- .../mediacodec/MediaCodecRenderer.java | 26 +++--- 9 files changed, 124 insertions(+), 142 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 d0d3b99973..66aea968da 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 @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.Assertions; @@ -185,42 +186,21 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } - // We have a format. - drmSession = pendingDrmSession; - ExoMediaCrypto mediaCrypto = null; - if (drmSession != null) { - int drmSessionState = drmSession.getState(); - if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); - } else if (drmSessionState == DrmSession.STATE_OPENED - || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) { - mediaCrypto = drmSession.getMediaCrypto(); - } else { - // The drm session isn't open yet. - return; - } - } - try { - if (decoder == null) { - // If we don't have a decoder yet, we need to instantiate one. - long codecInitializingTimestamp = SystemClock.elapsedRealtime(); - TraceUtil.beginSection("createVpxDecoder"); - decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, mediaCrypto); - decoder.setOutputMode(outputMode); + // If we don't have a decoder yet, we need to instantiate one. + maybeInitDecoder(); + + if (decoder != null) { + try { + // Rendering loop. + TraceUtil.beginSection("drainAndFeed"); + while (drainOutputBuffer(positionUs)) {} + while (feedInputBuffer()) {} TraceUtil.endSection(); - long codecInitializedTimestamp = SystemClock.elapsedRealtime(); - eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp, - codecInitializedTimestamp - codecInitializingTimestamp); - decoderCounters.decoderInitCount++; + } catch (VpxDecoderException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); } - TraceUtil.beginSection("drainAndFeed"); - while (drainOutputBuffer(positionUs)) {} - while (feedInputBuffer()) {} - TraceUtil.endSection(); - } catch (VpxDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + decoderCounters.ensureUpdated(); } - decoderCounters.ensureUpdated(); } private boolean drainOutputBuffer(long positionUs) throws VpxDecoderException { @@ -399,15 +379,14 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (drmSession == null) { + if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { return false; } - int drmSessionState = drmSession.getState(); + @DrmSession.State int drmSessionState = drmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); } - return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS - && (bufferEncrypted || !playClearSamplesWithoutKeys); + return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } private void flushDecoder() { @@ -516,6 +495,40 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } + private void maybeInitDecoder() throws ExoPlaybackException { + if (decoder != null) { + return; + } + + drmSession = pendingDrmSession; + ExoMediaCrypto mediaCrypto = null; + if (drmSession != null) { + mediaCrypto = drmSession.getMediaCrypto(); + if (mediaCrypto == null) { + DrmSessionException drmError = drmSession.getError(); + if (drmError != null) { + throw ExoPlaybackException.createForRenderer(drmError, getIndex()); + } + // The drm session isn't open yet. + return; + } + } + + try { + long codecInitializingTimestamp = SystemClock.elapsedRealtime(); + TraceUtil.beginSection("createVpxDecoder"); + decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, mediaCrypto); + decoder.setOutputMode(outputMode); + TraceUtil.endSection(); + long codecInitializedTimestamp = SystemClock.elapsedRealtime(); + eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp, + codecInitializedTimestamp - codecInitializingTimestamp); + decoderCounters.decoderInitCount++; + } catch (VpxDecoderException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } + } + private void releaseDecoder() { if (decoder != null) { decoder.release(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index f18e40dec4..79cb26bf39 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -1463,7 +1463,7 @@ public final class AudioTrack { @TargetApi(21) private int writeNonBlockingWithAvSyncV21(android.media.AudioTrack audioTrack, ByteBuffer buffer, int size, long presentationTimeUs) { - // TODO: Uncomment this when [Internal ref b/33627517] is clarified or fixed. + // TODO: Uncomment this when [Internal ref: b/33627517] is clarified or fixed. // if (Util.SDK_INT >= 23) { // // The underlying platform AudioTrack writes AV sync headers directly. // return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000); 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 a16a3f3ca2..c4a55eeb02 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 @@ -32,6 +32,7 @@ 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.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.Assertions; @@ -376,15 +377,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (drmSession == null) { + if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { return false; } @DrmSession.State int drmSessionState = drmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); } - return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS - && (bufferEncrypted || !playClearSamplesWithoutKeys); + return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } private void processEndOfStream() throws ExoPlaybackException { @@ -514,13 +514,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements drmSession = pendingDrmSession; ExoMediaCrypto mediaCrypto = null; if (drmSession != null) { - @DrmSession.State int drmSessionState = drmSession.getState(); - if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); - } else if (drmSessionState == DrmSession.STATE_OPENED - || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) { - mediaCrypto = drmSession.getMediaCrypto(); - } else { + mediaCrypto = drmSession.getMediaCrypto(); + if (mediaCrypto == null) { + DrmSessionException drmError = drmSession.getError(); + if (drmError != null) { + throw ExoPlaybackException.createForRenderer(drmError, getIndex()); + } // The drm session isn't open yet. return; } 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 cee174adbd..68eba76b11 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 @@ -222,7 +222,6 @@ public class DefaultDrmSessionManager implements DrmSe this.eventHandler = eventHandler; this.eventListener = eventListener; mediaDrm.setOnEventListener(new MediaDrmEventListener()); - state = STATE_CLOSED; mode = MODE_PLAYBACK; } @@ -358,7 +357,7 @@ public class DefaultDrmSessionManager implements DrmSe if (--openCount != 0) { return; } - state = STATE_CLOSED; + state = STATE_RELEASED; provisioningInProgress = false; mediaDrmHandler.removeCallbacksAndMessages(null); postResponseHandler.removeCallbacksAndMessages(null); @@ -384,35 +383,19 @@ public class DefaultDrmSessionManager implements DrmSe return state; } - @Override - public final T getMediaCrypto() { - if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) { - throw new IllegalStateException(); - } - return mediaCrypto; - } - - @Override - public boolean requiresSecureDecoderComponent(String mimeType) { - if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) { - throw new IllegalStateException(); - } - return mediaCrypto.requiresSecureDecoderComponent(mimeType); - } - @Override public final DrmSessionException getError() { return state == STATE_ERROR ? lastException : null; } + @Override + public final T getMediaCrypto() { + return mediaCrypto; + } + @Override public Map queryKeyStatus() { - // User may call this method rightfully even if state == STATE_ERROR. So only check if there is - // a sessionId - if (sessionId == null) { - throw new IllegalStateException(); - } - return mediaDrm.queryKeyStatus(sessionId); + return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId); } @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 cd694396b7..cb0143db2c 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 @@ -41,16 +41,16 @@ public interface DrmSession { * The state of the DRM session. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({STATE_ERROR, STATE_CLOSED, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS}) + @IntDef({STATE_RELEASED, STATE_ERROR, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS}) @interface State {} + /** + * The session has been released. + */ + int STATE_RELEASED = 0; /** * The session has encountered an error. {@link #getError()} can be used to retrieve the cause. */ - int STATE_ERROR = 0; - /** - * The session is closed. - */ - int STATE_CLOSED = 1; + int STATE_ERROR = 1; /** * The session is being opened. */ @@ -65,66 +65,40 @@ public interface DrmSession { int STATE_OPENED_WITH_KEYS = 4; /** - * Returns the current state of the session. - * - * @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING}, - * {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}. + * Returns the current state of the session, which is one of {@link #STATE_ERROR}, + * {@link #STATE_RELEASED}, {@link #STATE_OPENING}, {@link #STATE_OPENED} and + * {@link #STATE_OPENED_WITH_KEYS}. */ @State int getState(); - /** - * Returns a {@link ExoMediaCrypto} for the open session. - *

        - * This method may be called when the session is in the following states: - * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS} - * - * @return A {@link ExoMediaCrypto} for the open session. - * @throws IllegalStateException If called when a session isn't opened. - */ - T getMediaCrypto(); - - /** - * Whether the session requires a secure decoder for the specified mime type. - *

        - * Normally this method should return - * {@link ExoMediaCrypto#requiresSecureDecoderComponent(String)}, however in some cases - * implementations may wish to modify the return value (i.e. to force a secure decoder even when - * one is not required). - *

        - * This method may be called when the session is in the following states: - * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS} - * - * @return Whether the open session requires a secure decoder for the specified mime type. - * @throws IllegalStateException If called when a session isn't opened. - */ - boolean requiresSecureDecoderComponent(String mimeType); - /** * Returns the cause of the error state. - *

        - * This method may be called when the session is in any state. - * - * @return An exception if the state is {@link #STATE_ERROR}. Null otherwise. */ DrmSessionException getError(); /** - * Returns an informative description of the key status for the session. The status is in the form - * of {name, value} pairs. - * - *

        Since DRM license policies vary by vendor, the specific status field names are determined by + * Returns a {@link ExoMediaCrypto} for the open session, or null if called before the session has + * been opened or after it's been released. + */ + T getMediaCrypto(); + + /** + * Returns a map describing the key status for the session, or null if called before the session + * has been opened or after it's been released. + *

        + * Since DRM license policies vary by vendor, the specific status field names are determined by * each DRM vendor. Refer to your DRM provider documentation for definitions of the field names * for a particular DRM engine plugin. * - * @return A map of key status. - * @throws IllegalStateException If called when the session isn't opened. + * @return A map describing the key status for the session, or null if called before the session + * has been opened or after it's been released. * @see MediaDrm#queryKeyStatus(byte[]) */ Map queryKeyStatus(); /** - * Returns the key set id of the offline license loaded into this session, if there is one. Null - * otherwise. + * Returns the key set id of the offline license loaded into this session, or null if there isn't + * one. */ byte[] getOfflineLicenseKeySetId(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java index dd441a022f..5bee85f449 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java @@ -26,9 +26,12 @@ import com.google.android.exoplayer2.util.Assertions; public final class FrameworkMediaCrypto implements ExoMediaCrypto { private final MediaCrypto mediaCrypto; + private final boolean forceAllowInsecureDecoderComponents; - /* package */ FrameworkMediaCrypto(MediaCrypto mediaCrypto) { + /* package */ FrameworkMediaCrypto(MediaCrypto mediaCrypto, + boolean forceAllowInsecureDecoderComponents) { this.mediaCrypto = Assertions.checkNotNull(mediaCrypto); + this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents; } public MediaCrypto getWrappedMediaCrypto() { @@ -37,7 +40,8 @@ public final class FrameworkMediaCrypto implements ExoMediaCrypto { @Override public boolean requiresSecureDecoderComponent(String mimeType) { - return mediaCrypto.requiresSecureDecoderComponent(mimeType); + return !forceAllowInsecureDecoderComponents + && mediaCrypto.requiresSecureDecoderComponent(mimeType); } } 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 e6887af6da..ed4494559a 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 @@ -24,7 +24,9 @@ import android.media.NotProvisionedException; import android.media.ResourceBusyException; import android.media.UnsupportedSchemeException; import android.support.annotation.NonNull; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -163,7 +165,12 @@ public final class FrameworkMediaDrm implements ExoMediaDrm getLicenseDurationRemainingSec(DrmSession drmSession) { Map keyStatus = drmSession.queryKeyStatus(); - return new Pair<>( - getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING), + if (keyStatus == null) { + return null; + } + return new Pair<>(getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING), getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING)); } 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 750ba1f6ec..49b221d5b4 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 @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; @@ -298,20 +299,20 @@ public abstract class MediaCodecRenderer extends BaseRenderer { drmSession = pendingDrmSession; String mimeType = format.sampleMimeType; - MediaCrypto mediaCrypto = null; + MediaCrypto wrappedMediaCrypto = null; boolean drmSessionRequiresSecureDecoder = false; if (drmSession != null) { - @DrmSession.State int drmSessionState = drmSession.getState(); - if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); - } else if (drmSessionState == DrmSession.STATE_OPENED - || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) { - mediaCrypto = drmSession.getMediaCrypto().getWrappedMediaCrypto(); - drmSessionRequiresSecureDecoder = drmSession.requiresSecureDecoderComponent(mimeType); - } else { + FrameworkMediaCrypto mediaCrypto = drmSession.getMediaCrypto(); + if (mediaCrypto == null) { + DrmSessionException drmError = drmSession.getError(); + if (drmError != null) { + throw ExoPlaybackException.createForRenderer(drmError, getIndex()); + } // The drm session isn't open yet. return; } + wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto(); + drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType); } if (codecInfo == null) { @@ -358,7 +359,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codec = MediaCodec.createByCodecName(codecName); TraceUtil.endSection(); TraceUtil.beginSection("configureCodec"); - configureCodec(codecInfo, codec, format, mediaCrypto); + configureCodec(codecInfo, codec, format, wrappedMediaCrypto); TraceUtil.endSection(); TraceUtil.beginSection("startCodec"); codec.start(); @@ -736,15 +737,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (drmSession == null) { + if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { return false; } @DrmSession.State int drmSessionState = drmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); } - return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS - && (bufferEncrypted || !playClearSamplesWithoutKeys); + return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } /** From ba0e5246d66d1c348062d6065e32009c60e86a42 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 29 Jun 2017 09:48:28 -0700 Subject: [PATCH 199/220] Properly handle replacement of the DRM session in LibvpxVideoRenderer ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160537100 --- .../ext/vp9/LibvpxVideoRenderer.java | 125 +++++++++++++----- 1 file changed, 91 insertions(+), 34 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 66aea968da..9b0355a9e7 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 @@ -20,6 +20,7 @@ import android.graphics.Canvas; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; +import android.support.annotation.IntDef; import android.view.Surface; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; @@ -39,12 +40,35 @@ import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Decodes and renders video using the native VP9 decoder. */ public final class LibvpxVideoRenderer extends BaseRenderer { + @Retention(RetentionPolicy.SOURCE) + @IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM, + REINITIALIZATION_STATE_WAIT_END_OF_STREAM}) + private @interface ReinitializationState {} + /** + * The decoder does not need to be re-initialized. + */ + private static final int REINITIALIZATION_STATE_NONE = 0; + /** + * The input format has changed in a way that requires the decoder to be re-initialized, but we + * haven't yet signaled an end of stream to the existing decoder. We need to do so in order to + * ensure that it outputs any remaining buffers before we release it. + */ + private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1; + /** + * The input format has changed in a way that requires the decoder to be re-initialized, and we've + * signaled an end of stream to the existing decoder. We're waiting for the decoder to output an + * end of stream signal to indicate that it has output any remaining buffers before we release it. + */ + private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2; + /** * The type of a message that can be passed to an instance of this class via * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object @@ -78,6 +102,10 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private DrmSession drmSession; private DrmSession pendingDrmSession; + @ReinitializationState + private int decoderReinitializationState; + private boolean decoderReceivedBuffers; + private Bitmap bitmap; private boolean renderedFirstFrame; private long joiningDeadlineMs; @@ -154,6 +182,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); eventDispatcher = new EventDispatcher(eventHandler, eventListener); outputMode = VpxDecoder.OUTPUT_MODE_NONE; + decoderReinitializationState = REINITIALIZATION_STATE_NONE; } @Override @@ -203,11 +232,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } - private boolean drainOutputBuffer(long positionUs) throws VpxDecoderException { - if (outputStreamEnded) { - return false; - } - + private boolean drainOutputBuffer(long positionUs) throws ExoPlaybackException, + VpxDecoderException { // Acquire outputBuffer either from nextOutputBuffer or from the decoder. if (outputBuffer == null) { if (nextOutputBuffer != null) { @@ -227,9 +253,15 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } if (outputBuffer.isEndOfStream()) { - outputStreamEnded = true; - outputBuffer.release(); - outputBuffer = null; + if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) { + // We're waiting to re-initialize the decoder, and have now processed all final buffers. + releaseDecoder(); + maybeInitDecoder(); + } else { + outputBuffer.release(); + outputBuffer = null; + outputStreamEnded = true; + } return false; } @@ -333,7 +365,9 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException { - if (inputStreamEnded) { + if (decoder == null || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM + || inputStreamEnded) { + // We need to reinitialize the decoder or the input stream has ended. return false; } @@ -344,6 +378,14 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } + if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) { + inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + decoder.queueInputBuffer(inputBuffer); + inputBuffer = null; + decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM; + return false; + } + int result; if (waitingForKeys) { // We've already read an encrypted sample into buffer, and are waiting for keys. @@ -373,6 +415,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { inputBuffer.flip(); inputBuffer.colorInfo = formatHolder.format.colorInfo; decoder.queueInputBuffer(inputBuffer); + decoderReceivedBuffers = true; decoderCounters.inputBufferCount++; inputBuffer = null; return true; @@ -389,18 +432,24 @@ public final class LibvpxVideoRenderer extends BaseRenderer { return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } - private void flushDecoder() { - inputBuffer = null; + private void flushDecoder() throws ExoPlaybackException { waitingForKeys = false; - if (outputBuffer != null) { - outputBuffer.release(); - outputBuffer = null; + if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) { + releaseDecoder(); + maybeInitDecoder(); + } else { + inputBuffer = null; + if (outputBuffer != null) { + outputBuffer.release(); + outputBuffer = null; + } + if (nextOutputBuffer != null) { + nextOutputBuffer.release(); + nextOutputBuffer = null; + } + decoder.flush(); + decoderReceivedBuffers = false; } - if (nextOutputBuffer != null) { - nextOutputBuffer.release(); - nextOutputBuffer = null; - } - decoder.flush(); } @Override @@ -438,7 +487,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } @Override - protected void onPositionReset(long positionUs, boolean joining) { + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { inputStreamEnded = false; outputStreamEnded = false; clearRenderedFirstFrame(); @@ -467,8 +516,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer { @Override protected void onDisabled() { - inputBuffer = null; - outputBuffer = null; format = null; waitingForKeys = false; clearReportedVideoSize(); @@ -530,19 +577,18 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } private void releaseDecoder() { - if (decoder != null) { - decoder.release(); - decoder = null; - decoderCounters.decoderReleaseCount++; - waitingForKeys = false; - if (drmSession != null && pendingDrmSession != drmSession) { - try { - drmSessionManager.releaseSession(drmSession); - } finally { - drmSession = null; - } - } + if (decoder == null) { + return; } + + inputBuffer = null; + outputBuffer = null; + nextOutputBuffer = null; + decoder.release(); + decoder = null; + decoderCounters.decoderReleaseCount++; + decoderReinitializationState = REINITIALIZATION_STATE_NONE; + decoderReceivedBuffers = false; } private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { @@ -566,6 +612,17 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } + if (pendingDrmSession != drmSession) { + if (decoderReceivedBuffers) { + // Signal end of stream and wait for any final output buffers before re-initialization. + decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; + } else { + // There aren't any final output buffers, so release the decoder immediately. + releaseDecoder(); + maybeInitDecoder(); + } + } + eventDispatcher.inputFormatChanged(format); } From 84f4fe5cf9c5085a0631ace25644e9ad79ab06a6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 29 Jun 2017 09:48:46 -0700 Subject: [PATCH 200/220] Recover from empty timelines. (Related to GitHub #1706) When the timeline becomes empty, the playback state transitions to "ended". When the timeline becomes non-empty again, exceptions are thrown because MSG_DO_SOME_WORK is still regularly sent and media periods are getting prepared. This change ensures that no MSG_DO_SOME_WORK messages are sent in "ended" state. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160537147 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 6f54d5f9e1..f6a0bdd08e 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 @@ -618,7 +618,7 @@ import java.io.IOException; if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) { scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS); - } else if (enabledRenderers.length != 0) { + } else if (enabledRenderers.length != 0 && state != ExoPlayer.STATE_ENDED) { scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS); } else { handler.removeMessages(MSG_DO_SOME_WORK); @@ -831,7 +831,7 @@ import java.io.IOException; for (ExoPlayerMessage message : messages) { message.target.handleMessage(message.messageType, message.message); } - if (mediaSource != null) { + if (state == ExoPlayer.STATE_READY || state == ExoPlayer.STATE_BUFFERING) { // The message may have caused something to change that now requires us to do work. handler.sendEmptyMessage(MSG_DO_SOME_WORK); } From 28030b6c7eba2d3c7578db24761bee26c6b4f829 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Thu, 29 Jun 2017 10:33:14 -0700 Subject: [PATCH 201/220] Temp workaround for [Internal: b/63092960] ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160543009 --- .../exoplayer2/extractor/mp4/AtomParsers.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 f63010924c..80422c15e6 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 @@ -681,9 +681,11 @@ import java.util.List; drmInitData = drmInitData.copyWithSchemeType(schemeType); } parent.setPosition(childPosition); - } else { - drmInitData = null; } +// TODO: Uncomment the following part when b/63092960 is fixed. +// else { +// drmInitData = null; +// } List initializationData = null; String mimeType = null; @@ -857,9 +859,11 @@ import java.util.List; drmInitData = drmInitData.copyWithSchemeType(schemeType); } parent.setPosition(childPosition); - } else { - drmInitData = null; } +// TODO: Uncomment the following part when b/63092960 is fixed. +// else { +// drmInitData = null; +// } // If the atom type determines a MIME type, set it immediately. String mimeType = null; From 04b57f9c8e9147a1d7511259aedf645a41fb90e7 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 30 Jun 2017 02:52:20 -0700 Subject: [PATCH 202/220] Fix DvbParser bug Issue: #2957 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160628086 --- .../com/google/android/exoplayer2/text/dvb/DvbParser.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java index 96c8a89801..c0caf1e57a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java @@ -667,13 +667,15 @@ import java.util.List; int runLength = 0; int clutIndex = 0; int peek = data.readBits(2); - if (!data.readBit()) { + if (peek != 0x00) { runLength = 1; clutIndex = peek; } else if (data.readBit()) { runLength = 3 + data.readBits(3); clutIndex = data.readBits(2); - } else if (!data.readBit()) { + } else if (data.readBit()) { + runLength = 1; + } else { switch (data.readBits(2)) { case 0x00: endOfPixelCodeString = true; From 3b2cfa148ccb0a899484a3a47cb81ea2c72394c3 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 30 Jun 2017 03:14:40 -0700 Subject: [PATCH 203/220] Move Timeline assertions to testutils. Some parts of TimelineTest provided common assertions and helper classes for other tests. As such, they better fit into testutils. In line with other assertion methods, the TimelineVerifier class has been converted to a set of static assertion methods. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160629797 --- .../android/exoplayer2/TimelineTest.java | 268 ++---------------- .../source/ClippingMediaSourceTest.java | 28 +- .../source/ConcatenatingMediaSourceTest.java | 100 +++---- .../DynamicConcatenatingMediaSourceTest.java | 109 ++++--- .../source/LoopingMediaSourceTest.java | 71 ++--- .../exoplayer2/testutil/TimelineAsserts.java | 261 +++++++++++++++++ 6 files changed, 433 insertions(+), 404 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java index 699c620da9..bf6ee31165 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java @@ -15,14 +15,8 @@ */ package com.google.android.exoplayer2; -import com.google.android.exoplayer2.ExoPlayer.RepeatMode; -import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.Timeline.Window; -import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSource.Listener; -import com.google.android.exoplayer2.upstream.Allocator; -import java.io.IOException; +import com.google.android.exoplayer2.testutil.TimelineAsserts; +import com.google.android.exoplayer2.testutil.TimelineAsserts.FakeTimeline; import junit.framework.TestCase; /** @@ -30,255 +24,31 @@ import junit.framework.TestCase; */ public class TimelineTest extends TestCase { - /** - * Fake timeline with multiple periods and user-defined window id. - */ - public static final class FakeTimeline extends Timeline { - - private static final int WINDOW_DURATION_US = 1000000; - - private final int periodCount; - private final int id; - - public FakeTimeline(int periodCount, int id) { - this.periodCount = periodCount; - this.id = id; - } - - @Override - public int getWindowCount() { - return 1; - } - - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - return window.set(id, 0, 0, true, false, 0, WINDOW_DURATION_US, 0, periodCount - 1, 0); - } - - @Override - public int getPeriodCount() { - return periodCount; - } - - @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - return period.set(periodIndex, null, 0, WINDOW_DURATION_US, 0); - } - - @Override - public int getIndexOfPeriod(Object uid) { - return C.INDEX_UNSET; - } - } - - /** - * Stub media source which returns a provided timeline as source info and keeps track if it is - * prepared and released. - */ - public static class StubMediaSource implements MediaSource { - private final Timeline timeline; - - private boolean isPrepared; - private volatile boolean isReleased; - - public StubMediaSource(Timeline timeline) { - this.timeline = timeline; - } - - @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - assertFalse(isPrepared); - listener.onSourceInfoRefreshed(timeline, null); - isPrepared = true; - } - - @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - } - - @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - return null; - } - - @Override - public void releasePeriod(MediaPeriod mediaPeriod) { - } - - @Override - public void releaseSource() { - assertTrue(isPrepared); - isReleased = true; - } - - public void assertReleased() { - assertTrue(isReleased); - } - } - - /** - * Extracts the timeline from a media source. - */ - public static Timeline extractTimelineFromMediaSource(MediaSource mediaSource) { - class TimelineListener implements Listener { - private Timeline timeline; - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - this.timeline = timeline; - } - } - TimelineListener listener = new TimelineListener(); - mediaSource.prepareSource(null, true, listener); - return listener.timeline; - } - - /** - * Verify the behaviour of {@link Timeline#getNextWindowIndex(int, int)}, - * {@link Timeline#getPreviousWindowIndex(int, int)}, - * {@link Timeline#getWindow(int, Window, boolean)}, - * {@link Timeline#getNextPeriodIndex(int, Period, Window, int)}, and - * {@link Timeline#getPeriod(int, Period, boolean)}. - */ - public static final class TimelineVerifier { - - private final Timeline timeline; - - public TimelineVerifier(Timeline timeline) { - this.timeline = timeline; - } - - public TimelineVerifier assertEmpty() { - assertWindowIds(); - assertPeriodCounts(); - return this; - } - - /** - * @param expectedWindowIds A list of expected window IDs. If an ID is unknown or not important - * {@code null} can be passed to skip this window. - */ - public TimelineVerifier assertWindowIds(Object... expectedWindowIds) { - Window window = new Window(); - assertEquals(expectedWindowIds.length, timeline.getWindowCount()); - for (int i = 0; i < timeline.getWindowCount(); i++) { - timeline.getWindow(i, window, true); - if (expectedWindowIds[i] != null) { - assertEquals(expectedWindowIds[i], window.id); - } - } - return this; - } - - public TimelineVerifier assertWindowIsDynamic(boolean... windowIsDynamic) { - Window window = new Window(); - for (int i = 0; i < timeline.getWindowCount(); i++) { - timeline.getWindow(i, window, true); - assertEquals(windowIsDynamic[i], window.isDynamic); - } - return this; - } - - public TimelineVerifier assertPreviousWindowIndices(@RepeatMode int repeatMode, - int... expectedPreviousWindowIndices) { - for (int i = 0; i < timeline.getWindowCount(); i++) { - assertEquals(expectedPreviousWindowIndices[i], - timeline.getPreviousWindowIndex(i, repeatMode)); - } - return this; - } - - public TimelineVerifier assertNextWindowIndices(@RepeatMode int repeatMode, - int... expectedNextWindowIndices) { - for (int i = 0; i < timeline.getWindowCount(); i++) { - assertEquals(expectedNextWindowIndices[i], - timeline.getNextWindowIndex(i, repeatMode)); - } - return this; - } - - public TimelineVerifier assertPeriodCounts(int... expectedPeriodCounts) { - int windowCount = timeline.getWindowCount(); - int[] accumulatedPeriodCounts = new int[windowCount + 1]; - accumulatedPeriodCounts[0] = 0; - for (int i = 0; i < windowCount; i++) { - accumulatedPeriodCounts[i + 1] = accumulatedPeriodCounts[i] + expectedPeriodCounts[i]; - } - assertEquals(accumulatedPeriodCounts[accumulatedPeriodCounts.length - 1], - timeline.getPeriodCount()); - Window window = new Window(); - Period period = new Period(); - for (int i = 0; i < windowCount; i++) { - timeline.getWindow(i, window, true); - assertEquals(accumulatedPeriodCounts[i], window.firstPeriodIndex); - assertEquals(accumulatedPeriodCounts[i + 1] - 1, window.lastPeriodIndex); - } - int expectedWindowIndex = 0; - for (int i = 0; i < timeline.getPeriodCount(); i++) { - timeline.getPeriod(i, period, true); - while (i >= accumulatedPeriodCounts[expectedWindowIndex + 1]) { - expectedWindowIndex++; - } - assertEquals(expectedWindowIndex, period.windowIndex); - if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) { - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, - ExoPlayer.REPEAT_MODE_OFF)); - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, - ExoPlayer.REPEAT_MODE_ONE)); - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, - ExoPlayer.REPEAT_MODE_ALL)); - } else { - int nextWindowOff = timeline.getNextWindowIndex(expectedWindowIndex, - ExoPlayer.REPEAT_MODE_OFF); - int nextWindowOne = timeline.getNextWindowIndex(expectedWindowIndex, - ExoPlayer.REPEAT_MODE_ONE); - int nextWindowAll = timeline.getNextWindowIndex(expectedWindowIndex, - ExoPlayer.REPEAT_MODE_ALL); - int nextPeriodOff = nextWindowOff == C.INDEX_UNSET ? C.INDEX_UNSET - : accumulatedPeriodCounts[nextWindowOff]; - int nextPeriodOne = nextWindowOne == C.INDEX_UNSET ? C.INDEX_UNSET - : accumulatedPeriodCounts[nextWindowOne]; - int nextPeriodAll = nextWindowAll == C.INDEX_UNSET ? C.INDEX_UNSET - : accumulatedPeriodCounts[nextWindowAll]; - assertEquals(nextPeriodOff, timeline.getNextPeriodIndex(i, period, window, - ExoPlayer.REPEAT_MODE_OFF)); - assertEquals(nextPeriodOne, timeline.getNextPeriodIndex(i, period, window, - ExoPlayer.REPEAT_MODE_ONE)); - assertEquals(nextPeriodAll, timeline.getNextPeriodIndex(i, period, window, - ExoPlayer.REPEAT_MODE_ALL)); - } - } - return this; - } - } - public void testEmptyTimeline() { - new TimelineVerifier(Timeline.EMPTY).assertEmpty(); + TimelineAsserts.assertEmpty(Timeline.EMPTY); } public void testSinglePeriodTimeline() { Timeline timeline = new FakeTimeline(1, 111); - new TimelineVerifier(timeline) - .assertWindowIds(111) - .assertPeriodCounts(1) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertWindowIds(timeline, 111); + TimelineAsserts.assertPeriodCounts(timeline, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0); } public void testMultiPeriodTimeline() { Timeline timeline = new FakeTimeline(5, 111); - new TimelineVerifier(timeline) - .assertWindowIds(111) - .assertPeriodCounts(5) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertWindowIds(timeline, 111); + TimelineAsserts.assertPeriodCounts(timeline, 5); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 145f6fc179..dbfd276b42 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -21,10 +21,9 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; -import com.google.android.exoplayer2.TimelineTest; -import com.google.android.exoplayer2.TimelineTest.FakeTimeline; -import com.google.android.exoplayer2.TimelineTest.StubMediaSource; -import com.google.android.exoplayer2.TimelineTest.TimelineVerifier; +import com.google.android.exoplayer2.testutil.TimelineAsserts; +import com.google.android.exoplayer2.testutil.TimelineAsserts.FakeTimeline; +import com.google.android.exoplayer2.testutil.TimelineAsserts.StubMediaSource; /** * Unit tests for {@link ClippingMediaSource}. @@ -105,15 +104,16 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { Timeline timeline = new FakeTimeline(1, 111); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); - new TimelineVerifier(clippedTimeline) - .assertWindowIds(111) - .assertPeriodCounts(1) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertWindowIds(clippedTimeline, 111); + TimelineAsserts.assertPeriodCounts(clippedTimeline, 1); + TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_OFF, + C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertNextWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_OFF, + C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertNextWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ALL, 0); } /** @@ -121,7 +121,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { */ private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { MediaSource mediaSource = new StubMediaSource(timeline); - return TimelineTest.extractTimelineFromMediaSource( + return TimelineAsserts.extractTimelineFromMediaSource( new ClippingMediaSource(mediaSource, startMs, endMs)); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index a8a80517f2..c236679d88 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -18,10 +18,9 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.TimelineTest; -import com.google.android.exoplayer2.TimelineTest.FakeTimeline; -import com.google.android.exoplayer2.TimelineTest.StubMediaSource; -import com.google.android.exoplayer2.TimelineTest.TimelineVerifier; +import com.google.android.exoplayer2.testutil.TimelineAsserts; +import com.google.android.exoplayer2.testutil.TimelineAsserts.FakeTimeline; +import com.google.android.exoplayer2.testutil.TimelineAsserts.StubMediaSource; import junit.framework.TestCase; /** @@ -31,67 +30,68 @@ public final class ConcatenatingMediaSourceTest extends TestCase { public void testSingleMediaSource() { Timeline timeline = getConcatenatedTimeline(false, new FakeTimeline(3, 111)); - new TimelineVerifier(timeline) - .assertWindowIds(111) - .assertPeriodCounts(3) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertWindowIds(timeline, 111); + TimelineAsserts.assertPeriodCounts(timeline, 3); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0); timeline = getConcatenatedTimeline(true, new FakeTimeline(3, 111)); - new TimelineVerifier(timeline) - .assertWindowIds(111) - .assertPeriodCounts(3) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertWindowIds(timeline, 111); + TimelineAsserts.assertPeriodCounts(timeline, 3); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0); } public void testMultipleMediaSources() { Timeline[] timelines = { new FakeTimeline(3, 111), new FakeTimeline(1, 222), new FakeTimeline(3, 333) }; Timeline timeline = getConcatenatedTimeline(false, timelines); - new TimelineVerifier(timeline) - .assertWindowIds(111, 222, 333) - .assertPeriodCounts(3, 1, 3) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET, + 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); timeline = getConcatenatedTimeline(true, timelines); - new TimelineVerifier(timeline) - .assertWindowIds(111, 222, 333) - .assertPeriodCounts(3, 1, 3) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 2, 0, 1) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 1, 2, 0) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); } public void testNestedMediaSources() { Timeline timeline = getConcatenatedTimeline(false, getConcatenatedTimeline(false, new FakeTimeline(1, 111), new FakeTimeline(1, 222)), getConcatenatedTimeline(true, new FakeTimeline(1, 333), new FakeTimeline(1, 444))); - new TimelineVerifier(timeline) - .assertWindowIds(111, 222, 333, 444) - .assertPeriodCounts(1, 1, 1, 1) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1, 2) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 3, 2) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 3, 0, 1, 2) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, 1, 2, 3, C.INDEX_UNSET) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 3, 2) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 1, 2, 3, 0); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, + C.INDEX_UNSET, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 3, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, + 1, 2, 3, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 3, 2); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 3, 0); } /** @@ -104,7 +104,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { for (int i = 0; i < timelines.length; i++) { mediaSources[i] = new StubMediaSource(timelines[i]); } - return TimelineTest.extractTimelineFromMediaSource( + return TimelineAsserts.extractTimelineFromMediaSource( new ConcatenatingMediaSource(isRepeatOneAtomic, mediaSources)); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 0f63201964..a7281d7e21 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -19,15 +19,15 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.TimelineTest; -import com.google.android.exoplayer2.TimelineTest.FakeTimeline; -import com.google.android.exoplayer2.TimelineTest.StubMediaSource; -import com.google.android.exoplayer2.TimelineTest.TimelineVerifier; import com.google.android.exoplayer2.source.MediaSource.Listener; +import com.google.android.exoplayer2.testutil.TimelineAsserts; +import com.google.android.exoplayer2.testutil.TimelineAsserts.FakeTimeline; +import com.google.android.exoplayer2.testutil.TimelineAsserts.StubMediaSource; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; @@ -46,48 +46,43 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { public void testPlaylistChangesAfterPreparation() throws InterruptedException { timeline = null; - TimelineTest.StubMediaSource[] childSources = createMediaSources(7); + TimelineAsserts.StubMediaSource[] childSources = createMediaSources(7); DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); prepareAndListenToTimelineUpdates(mediaSource); waitForTimelineUpdate(); - new TimelineVerifier(timeline).assertEmpty(); + TimelineAsserts.assertEmpty(timeline); // Add first source. mediaSource.addMediaSource(childSources[0]); waitForTimelineUpdate(); assertNotNull(timeline); - new TimelineVerifier(timeline) - .assertPeriodCounts(1) - .assertWindowIds(111); + TimelineAsserts.assertPeriodCounts(timeline, 1); + TimelineAsserts.assertWindowIds(timeline, 111); // Add at front of queue. mediaSource.addMediaSource(0, childSources[1]); waitForTimelineUpdate(); - new TimelineVerifier(timeline) - .assertPeriodCounts(2, 1) - .assertWindowIds(222, 111); + TimelineAsserts.assertPeriodCounts(timeline, 2, 1); + TimelineAsserts.assertWindowIds(timeline, 222, 111); // Add at back of queue. mediaSource.addMediaSource(childSources[2]); waitForTimelineUpdate(); - new TimelineVerifier(timeline) - .assertPeriodCounts(2, 1, 3) - .assertWindowIds(222, 111, 333); + TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 111, 333); // Add in the middle. mediaSource.addMediaSource(1, childSources[3]); waitForTimelineUpdate(); - new TimelineVerifier(timeline) - .assertPeriodCounts(2, 4, 1, 3) - .assertWindowIds(222, 444, 111, 333); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 333); // Add bulk. mediaSource.addMediaSources(3, Arrays.asList((MediaSource) childSources[4], (MediaSource) childSources[5], (MediaSource) childSources[6])); waitForTimelineUpdate(); - new TimelineVerifier(timeline) - .assertPeriodCounts(2, 4, 1, 5, 6, 7, 3) - .assertWindowIds(222, 444, 111, 555, 666, 777, 333); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); // Remove in the middle. mediaSource.removeMediaSource(3); @@ -98,41 +93,46 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { waitForTimelineUpdate(); mediaSource.removeMediaSource(1); waitForTimelineUpdate(); - new TimelineVerifier(timeline) - .assertPeriodCounts(2, 1, 3) - .assertWindowIds(222, 111, 333); + TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 111, 333); for (int i = 3; i <= 6; i++) { childSources[i].assertReleased(); } + // Assert correct next and previous indices behavior after some insertions and removals. + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1); + // Remove at front of queue. mediaSource.removeMediaSource(0); waitForTimelineUpdate(); - new TimelineVerifier(timeline) - .assertPeriodCounts(1, 3) - .assertWindowIds(111, 333); + TimelineAsserts.assertPeriodCounts(timeline, 1, 3); + TimelineAsserts.assertWindowIds(timeline, 111, 333); childSources[1].assertReleased(); // Remove at back of queue. mediaSource.removeMediaSource(1); waitForTimelineUpdate(); - new TimelineVerifier(timeline) - .assertPeriodCounts(1) - .assertWindowIds(111); + TimelineAsserts.assertPeriodCounts(timeline, 1); + TimelineAsserts.assertWindowIds(timeline, 111); childSources[2].assertReleased(); // Remove last source. mediaSource.removeMediaSource(0); waitForTimelineUpdate(); - new TimelineVerifier(timeline) - .assertPeriodCounts() - .assertWindowIds(); + TimelineAsserts.assertEmpty(timeline); childSources[3].assertReleased(); } public void testPlaylistChangesBeforePreparation() throws InterruptedException { timeline = null; - TimelineTest.StubMediaSource[] childSources = createMediaSources(4); + TimelineAsserts.StubMediaSource[] childSources = createMediaSources(4); DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); mediaSource.addMediaSource(childSources[0]); mediaSource.addMediaSource(childSources[1]); @@ -144,9 +144,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { prepareAndListenToTimelineUpdates(mediaSource); waitForTimelineUpdate(); assertNotNull(timeline); - new TimelineVerifier(timeline) - .assertPeriodCounts(3, 4, 2) - .assertWindowIds(333, 444, 222); + TimelineAsserts.assertPeriodCounts(timeline, 3, 4, 2); + TimelineAsserts.assertWindowIds(timeline, 333, 444, 222); mediaSource.releaseSource(); for (int i = 1; i < 4; i++) { @@ -156,7 +155,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { public void testPlaylistWithLazyMediaSource() throws InterruptedException { timeline = null; - TimelineTest.StubMediaSource[] childSources = createMediaSources(2); + TimelineAsserts.StubMediaSource[] childSources = createMediaSources(2); LazyMediaSource[] lazySources = new LazyMediaSource[4]; for (int i = 0; i < 4; i++) { lazySources[i] = new LazyMediaSource(); @@ -172,17 +171,15 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { prepareAndListenToTimelineUpdates(mediaSource); waitForTimelineUpdate(); assertNotNull(timeline); - new TimelineVerifier(timeline) - .assertPeriodCounts(1, 1) - .assertWindowIds(111, null) - .assertWindowIsDynamic(false, true); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1); + TimelineAsserts.assertWindowIds(timeline, 111, null); + TimelineAsserts.assertWindowIsDynamic(timeline, false, true); lazySources[1].triggerTimelineUpdate(new FakeTimeline(9, 999)); waitForTimelineUpdate(); - new TimelineVerifier(timeline) - .assertPeriodCounts(1, 9) - .assertWindowIds(111, 999) - .assertWindowIsDynamic(false, false); + TimelineAsserts.assertPeriodCounts(timeline, 1, 9); + TimelineAsserts.assertWindowIds(timeline, 111, 999); + TimelineAsserts.assertWindowIsDynamic(timeline, false, false); //Add lazy sources after preparation mediaSource.addMediaSource(1, lazySources[2]); @@ -193,17 +190,15 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { waitForTimelineUpdate(); mediaSource.removeMediaSource(2); waitForTimelineUpdate(); - new TimelineVerifier(timeline) - .assertPeriodCounts(1, 1, 2, 9) - .assertWindowIds(null, 111, 222, 999) - .assertWindowIsDynamic(true, false, false, false); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 2, 9); + TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999); + TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false); lazySources[3].triggerTimelineUpdate(new FakeTimeline(8, 888)); waitForTimelineUpdate(); - new TimelineVerifier(timeline) - .assertPeriodCounts(8, 1, 2, 9) - .assertWindowIds(888, 111, 222, 999) - .assertWindowIsDynamic(false, false, false, false); + TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); + TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999); + TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false); mediaSource.releaseSource(); childSources[0].assertReleased(); @@ -272,10 +267,10 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { timelineUpdated = false; } - private TimelineTest.StubMediaSource[] createMediaSources(int count) { - TimelineTest.StubMediaSource[] sources = new TimelineTest.StubMediaSource[count]; + private TimelineAsserts.StubMediaSource[] createMediaSources(int count) { + TimelineAsserts.StubMediaSource[] sources = new TimelineAsserts.StubMediaSource[count]; for (int i = 0; i < count; i++) { - sources[i] = new TimelineTest.StubMediaSource(new FakeTimeline(i + 1, (i + 1) * 111)); + sources[i] = new TimelineAsserts.StubMediaSource(new FakeTimeline(i + 1, (i + 1) * 111)); } return sources; } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index dcb0c3d3bf..f9f35d20cf 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -18,10 +18,9 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.TimelineTest; -import com.google.android.exoplayer2.TimelineTest.FakeTimeline; -import com.google.android.exoplayer2.TimelineTest.StubMediaSource; -import com.google.android.exoplayer2.TimelineTest.TimelineVerifier; +import com.google.android.exoplayer2.testutil.TimelineAsserts; +import com.google.android.exoplayer2.testutil.TimelineAsserts.FakeTimeline; +import com.google.android.exoplayer2.testutil.TimelineAsserts.StubMediaSource; import junit.framework.TestCase; /** @@ -32,7 +31,7 @@ public class LoopingMediaSourceTest extends TestCase { private final Timeline multiWindowTimeline; public LoopingMediaSourceTest() { - multiWindowTimeline = TimelineTest.extractTimelineFromMediaSource( + multiWindowTimeline = TimelineAsserts.extractTimelineFromMediaSource( new ConcatenatingMediaSource( new StubMediaSource(new FakeTimeline(1, 111)), new StubMediaSource(new FakeTimeline(1, 222)), @@ -41,42 +40,46 @@ public class LoopingMediaSourceTest extends TestCase { public void testSingleLoop() { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1); - new TimelineVerifier(timeline) - .assertWindowIds(111, 222, 333) - .assertPeriodCounts(1, 1, 1) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); } public void testMultiLoop() { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3); - new TimelineVerifier(timeline) - .assertWindowIds(111, 222, 333, 111, 222, 333, 111, 222, 333) - .assertPeriodCounts(1, 1, 1, 1, 1, 1, 1, 1, 1) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, - C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2, 3, 4, 5, 6, 7, 8) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 8, 0, 1, 2, 3, 4, 5, 6, 7) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, 1, 2, 3, 4, 5, 6, 7, 8, C.INDEX_UNSET) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2, 3, 4, 5, 6, 7, 8) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 1, 2, 3, 4, 5, 6, 7, 8, 0); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, + C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, + 0, 1, 2, 3, 4, 5, 6, 7, 8); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, + 8, 0, 1, 2, 3, 4, 5, 6, 7); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, + 1, 2, 3, 4, 5, 6, 7, 8, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, + 0, 1, 2, 3, 4, 5, 6, 7, 8); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, + 1, 2, 3, 4, 5, 6, 7, 8, 0); } public void testInfiniteLoop() { Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE); - new TimelineVerifier(timeline) - .assertWindowIds(111, 222, 333) - .assertPeriodCounts(1, 1, 1) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_OFF, 2, 0, 1) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2) - .assertPreviousWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_OFF, 1, 2, 0) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2) - .assertNextWindowIndices(ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); } /** @@ -85,7 +88,7 @@ public class LoopingMediaSourceTest extends TestCase { */ private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) { MediaSource mediaSource = new StubMediaSource(timeline); - return TimelineTest.extractTimelineFromMediaSource( + return TimelineAsserts.extractTimelineFromMediaSource( new LoopingMediaSource(mediaSource, loopCount)); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java new file mode 100644 index 0000000000..7e7cf58cf3 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -0,0 +1,261 @@ +/* + * 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.testutil; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.Listener; +import com.google.android.exoplayer2.upstream.Allocator; +import java.io.IOException; + +/** + * Unit test for {@link Timeline}. + */ +public final class TimelineAsserts { + + private TimelineAsserts() {} + + /** + * Fake timeline with multiple periods and user-defined window id. + */ + public static final class FakeTimeline extends Timeline { + + private static final int WINDOW_DURATION_US = 1000000; + + private final int periodCount; + private final int id; + + public FakeTimeline(int periodCount, int id) { + this.periodCount = periodCount; + this.id = id; + } + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + return window.set(id, 0, 0, true, false, 0, WINDOW_DURATION_US, 0, periodCount - 1, 0); + } + + @Override + public int getPeriodCount() { + return periodCount; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + return period.set(periodIndex, null, 0, WINDOW_DURATION_US, 0); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return C.INDEX_UNSET; + } + } + + /** + * Stub media source which returns a provided timeline as source info and keeps track if it is + * prepared and released. + */ + public static class StubMediaSource implements MediaSource { + private final Timeline timeline; + + private boolean isPrepared; + private volatile boolean isReleased; + + public StubMediaSource(Timeline timeline) { + this.timeline = timeline; + } + + @Override + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + assertFalse(isPrepared); + listener.onSourceInfoRefreshed(timeline, null); + isPrepared = true; + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + return null; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + } + + @Override + public void releaseSource() { + assertTrue(isPrepared); + isReleased = true; + } + + public void assertReleased() { + assertTrue(isReleased); + } + } + + /** + * Extracts the timeline from a media source. + */ + public static Timeline extractTimelineFromMediaSource(MediaSource mediaSource) { + class TimelineListener implements Listener { + private Timeline timeline; + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + this.timeline = timeline; + } + } + TimelineListener listener = new TimelineListener(); + mediaSource.prepareSource(null, true, listener); + return listener.timeline; + } + + /** + * Assert that timeline is empty (i.e. has no windows or periods). + */ + public static void assertEmpty(Timeline timeline) { + assertWindowIds(timeline); + assertPeriodCounts(timeline); + } + + /** + * Asserts that window IDs are set correctly. + * + * @param expectedWindowIds A list of expected window IDs. If an ID is unknown or not important + * {@code null} can be passed to skip this window. + */ + public static void assertWindowIds(Timeline timeline, Object... expectedWindowIds) { + Window window = new Window(); + assertEquals(expectedWindowIds.length, timeline.getWindowCount()); + for (int i = 0; i < timeline.getWindowCount(); i++) { + timeline.getWindow(i, window, true); + if (expectedWindowIds[i] != null) { + assertEquals(expectedWindowIds[i], window.id); + } + } + } + + /** + * Asserts that window properties {@link Window}.isDynamic are set correctly.. + */ + public static void assertWindowIsDynamic(Timeline timeline, boolean... windowIsDynamic) { + Window window = new Window(); + for (int i = 0; i < timeline.getWindowCount(); i++) { + timeline.getWindow(i, window, true); + assertEquals(windowIsDynamic[i], window.isDynamic); + } + } + + /** + * Asserts that previous window indices for each window are set correctly depending on the repeat + * mode. + */ + public static void assertPreviousWindowIndices(Timeline timeline, + @ExoPlayer.RepeatMode int repeatMode, int... expectedPreviousWindowIndices) { + for (int i = 0; i < timeline.getWindowCount(); i++) { + assertEquals(expectedPreviousWindowIndices[i], + timeline.getPreviousWindowIndex(i, repeatMode)); + } + } + + /** + * Asserts that next window indices for each window are set correctly depending on the repeat + * mode. + */ + public static void assertNextWindowIndices(Timeline timeline, + @ExoPlayer.RepeatMode int repeatMode, int... expectedNextWindowIndices) { + for (int i = 0; i < timeline.getWindowCount(); i++) { + assertEquals(expectedNextWindowIndices[i], + timeline.getNextWindowIndex(i, repeatMode)); + } + } + + /** + * Asserts that period counts for each window are set correctly. Also asserts the correct setting + * of {@link Window}.firstPeriodIndex, {@link Window}.lastPeriodIndex, and the behavior of + * {@link Timeline}.getNextPeriodIndex. + */ + public static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCounts) { + int windowCount = timeline.getWindowCount(); + int[] accumulatedPeriodCounts = new int[windowCount + 1]; + accumulatedPeriodCounts[0] = 0; + for (int i = 0; i < windowCount; i++) { + accumulatedPeriodCounts[i + 1] = accumulatedPeriodCounts[i] + expectedPeriodCounts[i]; + } + assertEquals(accumulatedPeriodCounts[accumulatedPeriodCounts.length - 1], + timeline.getPeriodCount()); + Window window = new Window(); + Period period = new Period(); + for (int i = 0; i < windowCount; i++) { + timeline.getWindow(i, window, true); + assertEquals(accumulatedPeriodCounts[i], window.firstPeriodIndex); + assertEquals(accumulatedPeriodCounts[i + 1] - 1, window.lastPeriodIndex); + } + int expectedWindowIndex = 0; + for (int i = 0; i < timeline.getPeriodCount(); i++) { + timeline.getPeriod(i, period, true); + while (i >= accumulatedPeriodCounts[expectedWindowIndex + 1]) { + expectedWindowIndex++; + } + assertEquals(expectedWindowIndex, period.windowIndex); + if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) { + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, + ExoPlayer.REPEAT_MODE_OFF)); + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, + ExoPlayer.REPEAT_MODE_ONE)); + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, + ExoPlayer.REPEAT_MODE_ALL)); + } else { + int nextWindowOff = timeline.getNextWindowIndex(expectedWindowIndex, + ExoPlayer.REPEAT_MODE_OFF); + int nextWindowOne = timeline.getNextWindowIndex(expectedWindowIndex, + ExoPlayer.REPEAT_MODE_ONE); + int nextWindowAll = timeline.getNextWindowIndex(expectedWindowIndex, + ExoPlayer.REPEAT_MODE_ALL); + int nextPeriodOff = nextWindowOff == C.INDEX_UNSET ? C.INDEX_UNSET + : accumulatedPeriodCounts[nextWindowOff]; + int nextPeriodOne = nextWindowOne == C.INDEX_UNSET ? C.INDEX_UNSET + : accumulatedPeriodCounts[nextWindowOne]; + int nextPeriodAll = nextWindowAll == C.INDEX_UNSET ? C.INDEX_UNSET + : accumulatedPeriodCounts[nextWindowAll]; + assertEquals(nextPeriodOff, timeline.getNextPeriodIndex(i, period, window, + ExoPlayer.REPEAT_MODE_OFF)); + assertEquals(nextPeriodOne, timeline.getNextPeriodIndex(i, period, window, + ExoPlayer.REPEAT_MODE_ONE)); + assertEquals(nextPeriodAll, timeline.getNextPeriodIndex(i, period, window, + ExoPlayer.REPEAT_MODE_ALL)); + } + } + } + +} From d54df32f25c55aaf37ee5cb5cf44c9f8d2d76625 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 30 Jun 2017 03:37:34 -0700 Subject: [PATCH 204/220] Correct JavaDoc in TimelineAsserts. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160631095 --- .../google/android/exoplayer2/testutil/TimelineAsserts.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index 7e7cf58cf3..6e50251c27 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -201,9 +201,9 @@ public final class TimelineAsserts { } /** - * Asserts that period counts for each window are set correctly. Also asserts the correct setting - * of {@link Window}.firstPeriodIndex, {@link Window}.lastPeriodIndex, and the behavior of - * {@link Timeline}.getNextPeriodIndex. + * Asserts that period counts for each window are set correctly. Also asserts that + * {@link Window#firstPeriodIndex} and {@link Window#lastPeriodIndex} are set correctly, and it + * asserts the correct behavior of {@link Timeline#getNextWindowIndex(int, int)}. */ public static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCounts) { int windowCount = timeline.getWindowCount(); From 9db0b8cce09c6ec72c2d58e192d4e744f399aa42 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 30 Jun 2017 04:11:22 -0700 Subject: [PATCH 205/220] Move fake ExoPlayer component test classes to testutils. Fake ExoPlayer componenets used within ExoPlayerTest.java can be useful for other test classes and are therefore made available in testutils. They can also be merged with other existing fake components. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160632908 --- .../android/exoplayer2/ExoPlayerTest.java | 533 +----------------- .../exoplayer2/testutil/ExoPlayerWrapper.java | 191 +++++++ .../testutil/FakeMediaClockRenderer.java | 36 ++ .../exoplayer2/testutil/FakeMediaPeriod.java | 124 ++++ .../exoplayer2/testutil/FakeMediaSource.java | 95 ++++ .../exoplayer2/testutil/FakeRenderer.java | 91 +++ .../exoplayer2/testutil/FakeSampleStream.java | 67 +++ .../exoplayer2/testutil/FakeTimeline.java | 84 +++ 8 files changed, 700 insertions(+), 521 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 8d76e8793f..3bc8805a76 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -15,28 +15,20 @@ */ package com.google.android.exoplayer2; -import android.os.Handler; -import android.os.HandlerThread; import android.util.Pair; -import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.MediaClock; +import com.google.android.exoplayer2.testutil.ExoPlayerWrapper; +import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; +import com.google.android.exoplayer2.testutil.FakeMediaSource; +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.util.MimeTypes; -import java.io.IOException; -import java.util.ArrayList; import java.util.LinkedList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import junit.framework.TestCase; /** @@ -62,7 +54,7 @@ public final class ExoPlayerTest extends TestCase { * error. */ public void testPlayEmptyTimeline() throws Exception { - PlayerWrapper playerWrapper = new PlayerWrapper(); + ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); Timeline timeline = Timeline.EMPTY; MediaSource mediaSource = new FakeMediaSource(timeline, null); FakeRenderer renderer = new FakeRenderer(null); @@ -79,7 +71,7 @@ public final class ExoPlayerTest extends TestCase { * Tests playback of a source that exposes a single period. */ public void testPlaySinglePeriodTimeline() throws Exception { - PlayerWrapper playerWrapper = new PlayerWrapper(); + ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); Object manifest = new Object(); MediaSource mediaSource = new FakeMediaSource(timeline, manifest, TEST_VIDEO_FORMAT); @@ -98,7 +90,7 @@ public final class ExoPlayerTest extends TestCase { * Tests playback of a source that exposes three periods. */ public void testPlayMultiPeriodTimeline() throws Exception { - PlayerWrapper playerWrapper = new PlayerWrapper(); + ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); Timeline timeline = new FakeTimeline( new TimelineWindowDefinition(false, false, 0), new TimelineWindowDefinition(false, false, 0), @@ -119,7 +111,7 @@ public final class ExoPlayerTest extends TestCase { * source. */ public void testReadAheadToEndDoesNotResetRenderer() throws Exception { - final PlayerWrapper playerWrapper = new PlayerWrapper(); + final ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); Timeline timeline = new FakeTimeline( new TimelineWindowDefinition(false, false, 10), new TimelineWindowDefinition(false, false, 10), @@ -166,7 +158,7 @@ public final class ExoPlayerTest extends TestCase { } public void testRepreparationGivesFreshSourceInfo() throws Exception { - PlayerWrapper playerWrapper = new PlayerWrapper(); + ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT); @@ -237,7 +229,7 @@ public final class ExoPlayerTest extends TestCase { int[] expectedWindowIndices = {1, 1, 2, 2, 0, 0, 0, 1, 2}; final LinkedList windowIndices = new LinkedList<>(); final CountDownLatch actionCounter = new CountDownLatch(actionSchedule.length); - PlayerWrapper playerWrapper = new PlayerWrapper() { + ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper() { @Override @SuppressWarnings("ResourceType") public void onPositionDiscontinuity() { @@ -268,505 +260,4 @@ public final class ExoPlayerTest extends TestCase { playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null)); } - /** - * Wraps a player with its own handler thread. - */ - private static class PlayerWrapper implements ExoPlayer.EventListener { - - private final CountDownLatch sourceInfoCountDownLatch; - private final CountDownLatch endedCountDownLatch; - private final HandlerThread playerThread; - private final Handler handler; - private final LinkedList> sourceInfos; - - /* package */ ExoPlayer player; - private TrackGroupArray trackGroups; - private Exception exception; - - // Written only on the main thread. - private volatile int positionDiscontinuityCount; - - public PlayerWrapper() { - sourceInfoCountDownLatch = new CountDownLatch(1); - endedCountDownLatch = new CountDownLatch(1); - playerThread = new HandlerThread("ExoPlayerTest thread"); - playerThread.start(); - handler = new Handler(playerThread.getLooper()); - sourceInfos = new LinkedList<>(); - } - - // Called on the test thread. - - public void blockUntilEnded(long timeoutMs) throws Exception { - if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { - exception = new TimeoutException("Test playback timed out waiting for playback to end."); - } - release(); - // Throw any pending exception (from playback, timing out or releasing). - if (exception != null) { - throw exception; - } - } - - public void blockUntilSourceInfoRefreshed(long timeoutMs) throws Exception { - if (!sourceInfoCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { - throw new TimeoutException("Test playback timed out waiting for source info."); - } - } - - public void setup(final MediaSource mediaSource, final Renderer... renderers) { - handler.post(new Runnable() { - @Override - public void run() { - try { - player = ExoPlayerFactory.newInstance(renderers, new DefaultTrackSelector()); - player.addListener(PlayerWrapper.this); - player.setPlayWhenReady(true); - player.prepare(mediaSource); - } catch (Exception e) { - handleError(e); - } - } - }); - } - - public void prepare(final MediaSource mediaSource) { - handler.post(new Runnable() { - @Override - public void run() { - try { - player.prepare(mediaSource); - } catch (Exception e) { - handleError(e); - } - } - }); - } - - public void release() throws InterruptedException { - handler.post(new Runnable() { - @Override - public void run() { - try { - if (player != null) { - player.release(); - } - } catch (Exception e) { - handleError(e); - } finally { - playerThread.quit(); - } - } - }); - playerThread.join(); - } - - private void handleError(Exception exception) { - if (this.exception == null) { - this.exception = exception; - } - endedCountDownLatch.countDown(); - } - - @SafeVarargs - public final void assertSourceInfosEquals(Pair... sourceInfos) { - assertEquals(sourceInfos.length, this.sourceInfos.size()); - for (Pair sourceInfo : sourceInfos) { - assertEquals(sourceInfo, this.sourceInfos.remove()); - } - } - - // ExoPlayer.EventListener implementation. - - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == ExoPlayer.STATE_ENDED) { - endedCountDownLatch.countDown(); - } - } - - @Override - public void onRepeatModeChanged(int repeatMode) { - // Do nothing. - } - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - sourceInfos.add(Pair.create(timeline, manifest)); - sourceInfoCountDownLatch.countDown(); - } - - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - this.trackGroups = trackGroups; - } - - @Override - public void onPlayerError(ExoPlaybackException exception) { - handleError(exception); - } - - @SuppressWarnings("NonAtomicVolatileUpdate") - @Override - public void onPositionDiscontinuity() { - positionDiscontinuityCount++; - } - - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - - } - - private static final class TimelineWindowDefinition { - - public final boolean isSeekable; - public final boolean isDynamic; - public final long durationUs; - - public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) { - this.isSeekable = isSeekable; - this.isDynamic = isDynamic; - this.durationUs = durationUs; - } - - } - - private static final class FakeTimeline extends Timeline { - - private final TimelineWindowDefinition[] windowDefinitions; - - public FakeTimeline(TimelineWindowDefinition... windowDefinitions) { - this.windowDefinitions = windowDefinitions; - } - - @Override - public int getWindowCount() { - return windowDefinitions.length; - } - - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; - Object id = setIds ? windowIndex : null; - return window.set(id, C.TIME_UNSET, C.TIME_UNSET, windowDefinition.isSeekable, - windowDefinition.isDynamic, 0, windowDefinition.durationUs, windowIndex, windowIndex, 0); - } - - @Override - public int getPeriodCount() { - return windowDefinitions.length; - } - - @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex]; - Object id = setIds ? periodIndex : null; - return period.set(id, id, periodIndex, windowDefinition.durationUs, 0); - } - - @Override - public int getIndexOfPeriod(Object uid) { - if (!(uid instanceof Integer)) { - return C.INDEX_UNSET; - } - int index = (Integer) uid; - return index >= 0 && index < windowDefinitions.length ? index : C.INDEX_UNSET; - } - - } - - /** - * Fake {@link MediaSource} that provides a given timeline (which must have one period). Creating - * the period will return a {@link FakeMediaPeriod}. - */ - private static class FakeMediaSource implements MediaSource { - - private final Timeline timeline; - private final Object manifest; - private final TrackGroupArray trackGroupArray; - private final ArrayList activeMediaPeriods; - - private boolean preparedSource; - private boolean releasedSource; - - public FakeMediaSource(Timeline timeline, Object manifest, Format... formats) { - this.timeline = timeline; - this.manifest = manifest; - TrackGroup[] trackGroups = new TrackGroup[formats.length]; - for (int i = 0; i < formats.length; i++) { - trackGroups[i] = new TrackGroup(formats[i]); - } - trackGroupArray = new TrackGroupArray(trackGroups); - activeMediaPeriods = new ArrayList<>(); - } - - @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - assertFalse(preparedSource); - preparedSource = true; - listener.onSourceInfoRefreshed(timeline, manifest); - } - - @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - assertTrue(preparedSource); - } - - @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - Assertions.checkIndex(id.periodIndex, 0, timeline.getPeriodCount()); - assertTrue(preparedSource); - assertFalse(releasedSource); - FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); - activeMediaPeriods.add(mediaPeriod); - return mediaPeriod; - } - - @Override - public void releasePeriod(MediaPeriod mediaPeriod) { - assertTrue(preparedSource); - assertFalse(releasedSource); - FakeMediaPeriod fakeMediaPeriod = (FakeMediaPeriod) mediaPeriod; - assertTrue(activeMediaPeriods.remove(fakeMediaPeriod)); - fakeMediaPeriod.release(); - } - - @Override - public void releaseSource() { - assertTrue(preparedSource); - assertFalse(releasedSource); - assertTrue(activeMediaPeriods.isEmpty()); - releasedSource = true; - } - - } - - /** - * Fake {@link MediaPeriod} that provides one track with a given {@link Format}. Selecting that - * track will give the player a {@link FakeSampleStream}. - */ - private static final class FakeMediaPeriod implements MediaPeriod { - - private final TrackGroupArray trackGroupArray; - - private boolean preparedPeriod; - - public FakeMediaPeriod(TrackGroupArray trackGroupArray) { - this.trackGroupArray = trackGroupArray; - } - - public void release() { - preparedPeriod = false; - } - - @Override - public void prepare(Callback callback, long positionUs) { - assertFalse(preparedPeriod); - assertEquals(0, positionUs); - preparedPeriod = true; - callback.onPrepared(this); - } - - @Override - public void maybeThrowPrepareError() throws IOException { - assertTrue(preparedPeriod); - } - - @Override - public TrackGroupArray getTrackGroups() { - assertTrue(preparedPeriod); - return trackGroupArray; - } - - @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - assertTrue(preparedPeriod); - int rendererCount = selections.length; - for (int i = 0; i < rendererCount; i++) { - if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { - streams[i] = null; - } - } - for (int i = 0; i < rendererCount; i++) { - if (streams[i] == null && selections[i] != null) { - TrackSelection selection = selections[i]; - assertEquals(1, selection.length()); - assertEquals(0, selection.getIndexInTrackGroup(0)); - TrackGroup trackGroup = selection.getTrackGroup(); - assertTrue(trackGroupArray.indexOf(trackGroup) != C.INDEX_UNSET); - streams[i] = new FakeSampleStream(trackGroup.getFormat(0)); - streamResetFlags[i] = true; - } - } - return 0; - } - - @Override - public void discardBuffer(long positionUs) { - // Do nothing. - } - - @Override - public long readDiscontinuity() { - assertTrue(preparedPeriod); - return C.TIME_UNSET; - } - - @Override - public long getBufferedPositionUs() { - assertTrue(preparedPeriod); - return C.TIME_END_OF_SOURCE; - } - - @Override - public long seekToUs(long positionUs) { - assertTrue(preparedPeriod); - return positionUs; - } - - @Override - public long getNextLoadPositionUs() { - assertTrue(preparedPeriod); - return C.TIME_END_OF_SOURCE; - } - - @Override - public boolean continueLoading(long positionUs) { - assertTrue(preparedPeriod); - return false; - } - - } - - /** - * Fake {@link SampleStream} that outputs a given {@link Format} then sets the end of stream flag - * on its input buffer. - */ - private static final class FakeSampleStream implements SampleStream { - - private final Format format; - - private boolean readFormat; - - public FakeSampleStream(Format format) { - this.format = format; - } - - @Override - public boolean isReady() { - return true; - } - - @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, - boolean formatRequired) { - if (formatRequired || !readFormat) { - formatHolder.format = format; - readFormat = true; - return C.RESULT_FORMAT_READ; - } else { - buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); - return C.RESULT_BUFFER_READ; - } - } - - @Override - public void maybeThrowError() throws IOException { - // Do nothing. - } - - @Override - public void skipData(long positionUs) { - // Do nothing. - } - - } - - /** - * Fake {@link Renderer} that supports any format with the matching MIME type. The renderer - * verifies that it reads a given {@link Format}. - */ - private static class FakeRenderer extends BaseRenderer { - - private final Format expectedFormat; - - public int positionResetCount; - public int formatReadCount; - public int bufferReadCount; - public boolean isEnded; - - public FakeRenderer(Format expectedFormat) { - super(expectedFormat == null ? C.TRACK_TYPE_UNKNOWN - : MimeTypes.getTrackType(expectedFormat.sampleMimeType)); - this.expectedFormat = expectedFormat; - } - - @Override - protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { - positionResetCount++; - isEnded = false; - } - - @Override - public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (isEnded) { - return; - } - - // Verify the format matches the expected format. - FormatHolder formatHolder = new FormatHolder(); - DecoderInputBuffer buffer = - new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); - int result = readSource(formatHolder, buffer, false); - if (result == C.RESULT_FORMAT_READ) { - formatReadCount++; - assertEquals(expectedFormat, formatHolder.format); - } else if (result == C.RESULT_BUFFER_READ) { - bufferReadCount++; - if (buffer.isEndOfStream()) { - isEnded = true; - } - } - } - - @Override - public boolean isReady() { - return isSourceReady(); - } - - @Override - public boolean isEnded() { - return isEnded; - } - - @Override - public int supportsFormat(Format format) throws ExoPlaybackException { - return getTrackType() == MimeTypes.getTrackType(format.sampleMimeType) ? FORMAT_HANDLED - : FORMAT_UNSUPPORTED_TYPE; - } - - } - - private abstract static class FakeMediaClockRenderer extends FakeRenderer implements MediaClock { - - public FakeMediaClockRenderer(Format expectedFormat) { - super(expectedFormat); - } - - @Override - public MediaClock getMediaClock() { - return this; - } - - } - } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java new file mode 100644 index 0000000000..ff819d722e --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java @@ -0,0 +1,191 @@ +/* + * 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.testutil; + +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Pair; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import java.util.LinkedList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import junit.framework.Assert; + +/** + * Wraps a player with its own handler thread. + */ +public class ExoPlayerWrapper implements ExoPlayer.EventListener { + + private final CountDownLatch sourceInfoCountDownLatch; + private final CountDownLatch endedCountDownLatch; + private final HandlerThread playerThread; + private final Handler handler; + private final LinkedList> sourceInfos; + + public ExoPlayer player; + public TrackGroupArray trackGroups; + public Exception exception; + + // Written only on the main thread. + public volatile int positionDiscontinuityCount; + + public ExoPlayerWrapper() { + sourceInfoCountDownLatch = new CountDownLatch(1); + endedCountDownLatch = new CountDownLatch(1); + playerThread = new HandlerThread("ExoPlayerTest thread"); + playerThread.start(); + handler = new Handler(playerThread.getLooper()); + sourceInfos = new LinkedList<>(); + } + + // Called on the test thread. + + public void blockUntilEnded(long timeoutMs) throws Exception { + if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { + exception = new TimeoutException("Test playback timed out waiting for playback to end."); + } + release(); + // Throw any pending exception (from playback, timing out or releasing). + if (exception != null) { + throw exception; + } + } + + public void blockUntilSourceInfoRefreshed(long timeoutMs) throws Exception { + if (!sourceInfoCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("Test playback timed out waiting for source info."); + } + } + + public void setup(final MediaSource mediaSource, final Renderer... renderers) { + handler.post(new Runnable() { + @Override + public void run() { + try { + player = ExoPlayerFactory.newInstance(renderers, new DefaultTrackSelector()); + player.addListener(ExoPlayerWrapper.this); + player.setPlayWhenReady(true); + player.prepare(mediaSource); + } catch (Exception e) { + handleError(e); + } + } + }); + } + + public void prepare(final MediaSource mediaSource) { + handler.post(new Runnable() { + @Override + public void run() { + try { + player.prepare(mediaSource); + } catch (Exception e) { + handleError(e); + } + } + }); + } + + public void release() throws InterruptedException { + handler.post(new Runnable() { + @Override + public void run() { + try { + if (player != null) { + player.release(); + } + } catch (Exception e) { + handleError(e); + } finally { + playerThread.quit(); + } + } + }); + playerThread.join(); + } + + private void handleError(Exception exception) { + if (this.exception == null) { + this.exception = exception; + } + endedCountDownLatch.countDown(); + } + + @SafeVarargs + public final void assertSourceInfosEquals(Pair... sourceInfos) { + Assert.assertEquals(sourceInfos.length, this.sourceInfos.size()); + for (Pair sourceInfo : sourceInfos) { + Assert.assertEquals(sourceInfo, this.sourceInfos.remove()); + } + } + + // ExoPlayer.EventListener implementation. + + @Override + public void onLoadingChanged(boolean isLoading) { + // Do nothing. + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (playbackState == ExoPlayer.STATE_ENDED) { + endedCountDownLatch.countDown(); + } + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + sourceInfos.add(Pair.create(timeline, manifest)); + sourceInfoCountDownLatch.countDown(); + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + this.trackGroups = trackGroups; + } + + @Override + public void onPlayerError(ExoPlaybackException exception) { + handleError(exception); + } + + @SuppressWarnings("NonAtomicVolatileUpdate") + @Override + public void onPositionDiscontinuity() { + positionDiscontinuityCount++; + } + + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // Do nothing. + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java new file mode 100644 index 0000000000..76b1060804 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java @@ -0,0 +1,36 @@ +/* + * 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.testutil; + +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.util.MediaClock; + +/** + * Fake abstract {@link Renderer} which is also a {@link MediaClock}. + */ +public abstract class FakeMediaClockRenderer extends FakeRenderer implements MediaClock { + + public FakeMediaClockRenderer(Format expectedFormat) { + super(expectedFormat); + } + + @Override + public MediaClock getMediaClock() { + return this; + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java new file mode 100644 index 0000000000..d00ca58e23 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -0,0 +1,124 @@ +/* + * 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.testutil; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import java.io.IOException; +import junit.framework.Assert; + +/** + * Fake {@link MediaPeriod} that provides one track with a given {@link Format}. Selecting that + * track will give the player a {@link FakeSampleStream}. + */ +public final class FakeMediaPeriod implements MediaPeriod { + + private final TrackGroupArray trackGroupArray; + + private boolean preparedPeriod; + + public FakeMediaPeriod(TrackGroupArray trackGroupArray) { + this.trackGroupArray = trackGroupArray; + } + + public void release() { + preparedPeriod = false; + } + + @Override + public void prepare(Callback callback, long positionUs) { + Assert.assertFalse(preparedPeriod); + Assert.assertEquals(0, positionUs); + preparedPeriod = true; + callback.onPrepared(this); + } + + @Override + public void maybeThrowPrepareError() throws IOException { + Assert.assertTrue(preparedPeriod); + } + + @Override + public TrackGroupArray getTrackGroups() { + Assert.assertTrue(preparedPeriod); + return trackGroupArray; + } + + @Override + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + Assert.assertTrue(preparedPeriod); + int rendererCount = selections.length; + for (int i = 0; i < rendererCount; i++) { + if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { + streams[i] = null; + } + } + for (int i = 0; i < rendererCount; i++) { + if (streams[i] == null && selections[i] != null) { + TrackSelection selection = selections[i]; + Assert.assertEquals(1, selection.length()); + Assert.assertEquals(0, selection.getIndexInTrackGroup(0)); + TrackGroup trackGroup = selection.getTrackGroup(); + Assert.assertTrue(trackGroupArray.indexOf(trackGroup) != C.INDEX_UNSET); + streams[i] = new FakeSampleStream(trackGroup.getFormat(0)); + streamResetFlags[i] = true; + } + } + return 0; + } + + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + + @Override + public long readDiscontinuity() { + Assert.assertTrue(preparedPeriod); + return C.TIME_UNSET; + } + + @Override + public long getBufferedPositionUs() { + Assert.assertTrue(preparedPeriod); + return C.TIME_END_OF_SOURCE; + } + + @Override + public long seekToUs(long positionUs) { + Assert.assertTrue(preparedPeriod); + return positionUs; + } + + @Override + public long getNextLoadPositionUs() { + Assert.assertTrue(preparedPeriod); + return C.TIME_END_OF_SOURCE; + } + + @Override + public boolean continueLoading(long positionUs) { + Assert.assertTrue(preparedPeriod); + return false; + } + +} 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 new file mode 100644 index 0000000000..bb274ce417 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -0,0 +1,95 @@ +/* + * 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.testutil; + +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Format; +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.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.ArrayList; +import junit.framework.Assert; + +/** + * Fake {@link MediaSource} that provides a given timeline (which must have one period). Creating + * the period will return a {@link FakeMediaPeriod}. + */ +public class FakeMediaSource implements MediaSource { + + private final Timeline timeline; + private final Object manifest; + private final TrackGroupArray trackGroupArray; + private final ArrayList activeMediaPeriods; + + private boolean preparedSource; + private boolean releasedSource; + + public FakeMediaSource(Timeline timeline, Object manifest, Format... formats) { + this.timeline = timeline; + this.manifest = manifest; + TrackGroup[] trackGroups = new TrackGroup[formats.length]; + for (int i = 0; i < formats.length; i++) { + trackGroups[i] = new TrackGroup(formats[i]); + } + trackGroupArray = new TrackGroupArray(trackGroups); + activeMediaPeriods = new ArrayList<>(); + } + + @Override + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assert.assertFalse(preparedSource); + preparedSource = true; + listener.onSourceInfoRefreshed(timeline, manifest); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + Assert.assertTrue(preparedSource); + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + Assertions.checkIndex(id.periodIndex, 0, timeline.getPeriodCount()); + Assert.assertTrue(preparedSource); + Assert.assertFalse(releasedSource); + FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); + activeMediaPeriods.add(mediaPeriod); + return mediaPeriod; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + Assert.assertTrue(preparedSource); + Assert.assertFalse(releasedSource); + FakeMediaPeriod fakeMediaPeriod = (FakeMediaPeriod) mediaPeriod; + Assert.assertTrue(activeMediaPeriods.remove(fakeMediaPeriod)); + fakeMediaPeriod.release(); + } + + @Override + public void releaseSource() { + Assert.assertTrue(preparedSource); + Assert.assertFalse(releasedSource); + Assert.assertTrue(activeMediaPeriods.isEmpty()); + releasedSource = true; + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java new file mode 100644 index 0000000000..dc67261912 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java @@ -0,0 +1,91 @@ +/* + * 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.testutil; + +import com.google.android.exoplayer2.BaseRenderer; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.util.MimeTypes; +import junit.framework.Assert; + +/** + * Fake {@link Renderer} that supports any format with the matching MIME type. The renderer + * verifies that it reads a given {@link Format}. + */ +public class FakeRenderer extends BaseRenderer { + + private final Format expectedFormat; + + public int positionResetCount; + public int formatReadCount; + public int bufferReadCount; + public boolean isEnded; + + public FakeRenderer(Format expectedFormat) { + super(expectedFormat == null ? C.TRACK_TYPE_UNKNOWN + : MimeTypes.getTrackType(expectedFormat.sampleMimeType)); + this.expectedFormat = expectedFormat; + } + + @Override + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { + positionResetCount++; + isEnded = false; + } + + @Override + public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + if (isEnded) { + return; + } + + // Verify the format matches the expected format. + FormatHolder formatHolder = new FormatHolder(); + DecoderInputBuffer buffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + int result = readSource(formatHolder, buffer, false); + if (result == C.RESULT_FORMAT_READ) { + formatReadCount++; + Assert.assertEquals(expectedFormat, formatHolder.format); + } else if (result == C.RESULT_BUFFER_READ) { + bufferReadCount++; + if (buffer.isEndOfStream()) { + isEnded = true; + } + } + } + + @Override + public boolean isReady() { + return isSourceReady(); + } + + @Override + public boolean isEnded() { + return isEnded; + } + + @Override + public int supportsFormat(Format format) throws ExoPlaybackException { + return getTrackType() == MimeTypes.getTrackType(format.sampleMimeType) ? FORMAT_HANDLED + : FORMAT_UNSUPPORTED_TYPE; + } + +} 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 new file mode 100644 index 0000000000..4e1e32980f --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java @@ -0,0 +1,67 @@ +/* + * 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.testutil; + +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.source.SampleStream; +import java.io.IOException; + +/** + * Fake {@link SampleStream} that outputs a given {@link Format} then sets the end of stream flag + * on its input buffer. + */ +public final class FakeSampleStream implements SampleStream { + + private final Format format; + + private boolean readFormat; + + public FakeSampleStream(Format format) { + this.format = format; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + if (formatRequired || !readFormat) { + formatHolder.format = format; + readFormat = true; + return C.RESULT_FORMAT_READ; + } else { + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } + } + + @Override + public void maybeThrowError() throws IOException { + // Do nothing. + } + + @Override + public void skipData(long positionUs) { + // Do nothing. + } + +} 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 new file mode 100644 index 0000000000..0b18b00adc --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -0,0 +1,84 @@ +/* + * 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.testutil; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; + +/** + * Fake {@link Timeline} which can be setup to return custom {@link TimelineWindowDefinition}s. + */ +public final class FakeTimeline extends Timeline { + + /** + * Definition used to define a {@link FakeTimeline}. + */ + public static final class TimelineWindowDefinition { + + public final boolean isSeekable; + public final boolean isDynamic; + public final long durationUs; + + public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) { + this.isSeekable = isSeekable; + this.isDynamic = isDynamic; + this.durationUs = durationUs; + } + + } + + private final TimelineWindowDefinition[] windowDefinitions; + + public FakeTimeline(TimelineWindowDefinition... windowDefinitions) { + this.windowDefinitions = windowDefinitions; + } + + @Override + public int getWindowCount() { + return windowDefinitions.length; + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; + Object id = setIds ? windowIndex : null; + return window.set(id, C.TIME_UNSET, C.TIME_UNSET, windowDefinition.isSeekable, + windowDefinition.isDynamic, 0, windowDefinition.durationUs, windowIndex, windowIndex, 0); + } + + @Override + public int getPeriodCount() { + return windowDefinitions.length; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex]; + Object id = setIds ? periodIndex : null; + return period.set(id, id, periodIndex, windowDefinition.durationUs, 0); + } + + @Override + public int getIndexOfPeriod(Object uid) { + if (!(uid instanceof Integer)) { + return C.INDEX_UNSET; + } + int index = (Integer) uid; + return index >= 0 && index < windowDefinitions.length ? index : C.INDEX_UNSET; + } + +} From b3a7f8774f305c48e88b560999672fcff0630c9c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 30 Jun 2017 04:12:29 -0700 Subject: [PATCH 206/220] Upgrade IMA dependencies and README ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160632943 --- extensions/ima/README.md | 5 ++--- extensions/ima/build.gradle | 15 ++------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/extensions/ima/README.md b/extensions/ima/README.md index 9ef37170d7..dd4603ef4e 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -36,9 +36,8 @@ section of the app. This is a preview version with some known issues: -* Midroll ads are not yet fully supported. `playAd` and `AD_STARTED` events are - sometimes delayed, meaning that midroll ads take a long time to start and the - ad overlay does not show immediately. * Tapping the 'More info' button on an ad in the demo app will pause the activity, which destroys the ImaAdsMediaSource. Played ad breaks will be shown to the user again if the demo app returns to the foreground. +* Ad loading timeouts are currently propagated as player errors, rather than + being silently handled by resuming content. diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 3f95fcd414..b2dd2ab97b 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -15,19 +15,8 @@ android { dependencies { compile project(modulePrefix + 'library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion - compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.6.0' - compile 'com.google.android.gms:play-services-ads:10.2.4' - // There exists a dependency chain: - // com.google.android.gms:play-services-ads:10.2.4 - // |-> com.google.android.gms:play-services-ads-lite:10.2.4 - // |-> com.google.android.gms:play-services-basement:10.2.4 - // |-> com.android.support:support-v4:24.0.0 - // The support-v4:24.0.0 module directly includes older versions of the same - // classes as com.android.support:support-annotations. We need to manually - // force it to the version we're using to avoid a compilation failure. This - // will become unnecessary when the support-v4 dependency in the chain above - // has been updated to 24.2.0 or later. - compile 'com.android.support:support-v4:' + supportLibraryVersion + compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4' + compile 'com.google.android.gms:play-services-ads:11.0.1' androidTestCompile project(modulePrefix + 'library') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion From 177a33bbee7c796d974f73954f292fb45d9201f7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 30 Jun 2017 06:02:22 -0700 Subject: [PATCH 207/220] Merge TimelineAsserts.StubMediaSource into FakeMediaSource. The StubMediaSource was a subset of the FakeMediaSource. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160638245 --- .../source/ClippingMediaSourceTest.java | 4 +- .../source/ConcatenatingMediaSourceTest.java | 4 +- .../DynamicConcatenatingMediaSourceTest.java | 21 ++++---- .../source/LoopingMediaSourceTest.java | 10 ++-- .../exoplayer2/testutil/FakeMediaSource.java | 8 ++- .../exoplayer2/testutil/TimelineAsserts.java | 50 ------------------- 6 files changed, 27 insertions(+), 70 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index dbfd276b42..a7b37e2e23 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -21,9 +21,9 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.TimelineAsserts; import com.google.android.exoplayer2.testutil.TimelineAsserts.FakeTimeline; -import com.google.android.exoplayer2.testutil.TimelineAsserts.StubMediaSource; /** * Unit tests for {@link ClippingMediaSource}. @@ -120,7 +120,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline. */ private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { - MediaSource mediaSource = new StubMediaSource(timeline); + MediaSource mediaSource = new FakeMediaSource(timeline, null); return TimelineAsserts.extractTimelineFromMediaSource( new ClippingMediaSource(mediaSource, startMs, endMs)); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index c236679d88..40551aa38f 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -18,9 +18,9 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.TimelineAsserts; import com.google.android.exoplayer2.testutil.TimelineAsserts.FakeTimeline; -import com.google.android.exoplayer2.testutil.TimelineAsserts.StubMediaSource; import junit.framework.TestCase; /** @@ -102,7 +102,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { Timeline... timelines) { MediaSource[] mediaSources = new MediaSource[timelines.length]; for (int i = 0; i < timelines.length; i++) { - mediaSources[i] = new StubMediaSource(timelines[i]); + mediaSources[i] = new FakeMediaSource(timelines[i], null); } return TimelineAsserts.extractTimelineFromMediaSource( new ConcatenatingMediaSource(isRepeatOneAtomic, mediaSources)); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index a7281d7e21..982d37c3a1 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -25,9 +25,9 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.Listener; +import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.TimelineAsserts; import com.google.android.exoplayer2.testutil.TimelineAsserts.FakeTimeline; -import com.google.android.exoplayer2.testutil.TimelineAsserts.StubMediaSource; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; @@ -46,7 +46,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { public void testPlaylistChangesAfterPreparation() throws InterruptedException { timeline = null; - TimelineAsserts.StubMediaSource[] childSources = createMediaSources(7); + FakeMediaSource[] childSources = createMediaSources(7); DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); prepareAndListenToTimelineUpdates(mediaSource); waitForTimelineUpdate(); @@ -132,7 +132,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { public void testPlaylistChangesBeforePreparation() throws InterruptedException { timeline = null; - TimelineAsserts.StubMediaSource[] childSources = createMediaSources(4); + FakeMediaSource[] childSources = createMediaSources(4); DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); mediaSource.addMediaSource(childSources[0]); mediaSource.addMediaSource(childSources[1]); @@ -155,7 +155,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { public void testPlaylistWithLazyMediaSource() throws InterruptedException { timeline = null; - TimelineAsserts.StubMediaSource[] childSources = createMediaSources(2); + FakeMediaSource[] childSources = createMediaSources(2); LazyMediaSource[] lazySources = new LazyMediaSource[4]; for (int i = 0; i < 4; i++) { lazySources[i] = new LazyMediaSource(); @@ -207,7 +207,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { public void testIllegalArguments() { DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); - MediaSource validSource = new StubMediaSource(new FakeTimeline(1, 1)); + MediaSource validSource = new FakeMediaSource(new FakeTimeline(1, 1), null); // Null sources. try { @@ -234,7 +234,10 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { // Expected. } - mediaSources = new MediaSource[] { new StubMediaSource(new FakeTimeline(1, 1)), validSource}; + mediaSources = new MediaSource[] { + new FakeMediaSource(new FakeTimeline(1, 1), null), + validSource + }; try { mediaSource.addMediaSources(Arrays.asList(mediaSources)); fail("Duplicate mediaSource not allowed."); @@ -267,10 +270,10 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { timelineUpdated = false; } - private TimelineAsserts.StubMediaSource[] createMediaSources(int count) { - TimelineAsserts.StubMediaSource[] sources = new TimelineAsserts.StubMediaSource[count]; + private FakeMediaSource[] createMediaSources(int count) { + FakeMediaSource[] sources = new FakeMediaSource[count]; for (int i = 0; i < count; i++) { - sources[i] = new TimelineAsserts.StubMediaSource(new FakeTimeline(i + 1, (i + 1) * 111)); + sources[i] = new FakeMediaSource(new FakeTimeline(i + 1, (i + 1) * 111), null); } return sources; } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index f9f35d20cf..6157487005 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -18,9 +18,9 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.TimelineAsserts; import com.google.android.exoplayer2.testutil.TimelineAsserts.FakeTimeline; -import com.google.android.exoplayer2.testutil.TimelineAsserts.StubMediaSource; import junit.framework.TestCase; /** @@ -33,9 +33,9 @@ public class LoopingMediaSourceTest extends TestCase { public LoopingMediaSourceTest() { multiWindowTimeline = TimelineAsserts.extractTimelineFromMediaSource( new ConcatenatingMediaSource( - new StubMediaSource(new FakeTimeline(1, 111)), - new StubMediaSource(new FakeTimeline(1, 222)), - new StubMediaSource(new FakeTimeline(1, 333)))); + new FakeMediaSource(new FakeTimeline(1, 111), null), + new FakeMediaSource(new FakeTimeline(1, 222), null), + new FakeMediaSource(new FakeTimeline(1, 333), null))); } public void testSingleLoop() { @@ -87,7 +87,7 @@ public class LoopingMediaSourceTest extends TestCase { * the looping timeline. */ private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) { - MediaSource mediaSource = new StubMediaSource(timeline); + MediaSource mediaSource = new FakeMediaSource(timeline, null); return TimelineAsserts.extractTimelineFromMediaSource( new LoopingMediaSource(mediaSource, loopCount)); } 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 bb274ce417..3a6b03ed5e 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 @@ -29,8 +29,8 @@ import java.util.ArrayList; import junit.framework.Assert; /** - * Fake {@link MediaSource} that provides a given timeline (which must have one period). Creating - * the period will return a {@link FakeMediaPeriod}. + * Fake {@link MediaSource} that provides a given timeline. Creating the period will return a + * {@link FakeMediaPeriod} with a {@link TrackGroupArray} using the given {@link Format}s. */ public class FakeMediaSource implements MediaSource { @@ -53,6 +53,10 @@ public class FakeMediaSource implements MediaSource { activeMediaPeriods = new ArrayList<>(); } + public void assertReleased() { + Assert.assertTrue(releasedSource); + } + @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { Assert.assertFalse(preparedSource); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index 6e50251c27..97fd9b07ea 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -16,19 +16,14 @@ package com.google.android.exoplayer2.testutil; import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertTrue; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; -import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.Listener; -import com.google.android.exoplayer2.upstream.Allocator; -import java.io.IOException; /** * Unit test for {@link Timeline}. @@ -79,51 +74,6 @@ public final class TimelineAsserts { } } - /** - * Stub media source which returns a provided timeline as source info and keeps track if it is - * prepared and released. - */ - public static class StubMediaSource implements MediaSource { - private final Timeline timeline; - - private boolean isPrepared; - private volatile boolean isReleased; - - public StubMediaSource(Timeline timeline) { - this.timeline = timeline; - } - - @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - assertFalse(isPrepared); - listener.onSourceInfoRefreshed(timeline, null); - isPrepared = true; - } - - @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - } - - @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - return null; - } - - @Override - public void releasePeriod(MediaPeriod mediaPeriod) { - } - - @Override - public void releaseSource() { - assertTrue(isPrepared); - isReleased = true; - } - - public void assertReleased() { - assertTrue(isReleased); - } - } - /** * Extracts the timeline from a media source. */ From 51b98e817c09f58b5c5690d458c935feb9271b49 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 30 Jun 2017 06:05:42 -0700 Subject: [PATCH 208/220] Make Android Studio happy (make State public) ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160638478 --- .../main/java/com/google/android/exoplayer2/drm/DrmSession.java | 2 +- 1 file changed, 1 insertion(+), 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 cb0143db2c..0c17b102fd 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 @@ -42,7 +42,7 @@ public interface DrmSession { */ @Retention(RetentionPolicy.SOURCE) @IntDef({STATE_RELEASED, STATE_ERROR, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS}) - @interface State {} + public @interface State {} /** * The session has been released. */ From c7924bfe222481e9e7002a1f9d4599110de2930c Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 30 Jun 2017 07:52:35 -0700 Subject: [PATCH 209/220] Clean up parseSampleEntryEncryptionData It was a bit strange how it returned something via the return value and something else via the "out" variable, and doing it this way wasn't even saving any allocations. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160645640 --- .../exoplayer2/extractor/mp4/AtomParsers.java | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) 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 80422c15e6..ba190351c3 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 @@ -674,18 +674,19 @@ import java.util.List; int childPosition = parent.getPosition(); if (atomType == Atom.TYPE_encv) { - atomType = parseSampleEntryEncryptionData(parent, position, size, out, entryIndex); - TrackEncryptionBox encryptionBox = out.trackEncryptionBoxes[entryIndex]; - String schemeType = encryptionBox != null ? encryptionBox.schemeType : null; - if (schemeType != null) { - drmInitData = drmInitData.copyWithSchemeType(schemeType); + Pair sampleEntryEncryptionData = parseSampleEntryEncryptionData( + parent, position, size); + if (sampleEntryEncryptionData != null) { + atomType = sampleEntryEncryptionData.first; + drmInitData = drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType); + out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second; } parent.setPosition(childPosition); } -// TODO: Uncomment the following part when b/63092960 is fixed. -// else { -// drmInitData = null; -// } + // TODO: Uncomment when [Internal: b/63092960] is fixed. + // else { + // drmInitData = null; + // } List initializationData = null; String mimeType = null; @@ -852,18 +853,19 @@ import java.util.List; int childPosition = parent.getPosition(); if (atomType == Atom.TYPE_enca) { - atomType = parseSampleEntryEncryptionData(parent, position, size, out, entryIndex); - TrackEncryptionBox encryptionBox = out.trackEncryptionBoxes[entryIndex]; - String schemeType = encryptionBox != null ? encryptionBox.schemeType : null; - if (schemeType != null) { - drmInitData = drmInitData.copyWithSchemeType(schemeType); + Pair sampleEntryEncryptionData = parseSampleEntryEncryptionData( + parent, position, size); + if (sampleEntryEncryptionData != null) { + atomType = sampleEntryEncryptionData.first; + drmInitData = drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType); + out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second; } parent.setPosition(childPosition); } -// TODO: Uncomment the following part when b/63092960 is fixed. -// else { -// drmInitData = null; -// } + // TODO: Uncomment when [Internal: b/63092960] is fixed. + // else { + // drmInitData = null; + // } // If the atom type determines a MIME type, set it immediately. String mimeType = null; @@ -1039,11 +1041,12 @@ import java.util.List; } /** - * Parses encryption data from an audio/video sample entry, populating {@code out} and returning - * the unencrypted atom type, or 0 if no common encryption sinf atom was present. + * Parses encryption data from an audio/video sample entry, returning a pair consisting of the + * unencrypted atom type and a {@link TrackEncryptionBox}. Null is returned if no common + * encryption sinf atom was present. */ - private static int parseSampleEntryEncryptionData(ParsableByteArray parent, int position, - int size, StsdData out, int entryIndex) { + private static Pair parseSampleEntryEncryptionData( + ParsableByteArray parent, int position, int size) { int childPosition = parent.getPosition(); while (childPosition - position < size) { parent.setPosition(childPosition); @@ -1054,14 +1057,12 @@ import java.util.List; Pair result = parseSinfFromParent(parent, childPosition, childAtomSize); if (result != null) { - out.trackEncryptionBoxes[entryIndex] = result.second; - return result.first; + return result; } } childPosition += childAtomSize; } - // This enca/encv box does not have a data format so return an invalid atom type. - return 0; + return null; } private static Pair parseSinfFromParent(ParsableByteArray parent, From 4ee0b2e1c86a714bc87206d4ae42fde9b3233da8 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 30 Jun 2017 10:48:03 -0700 Subject: [PATCH 210/220] Update release notes + bump version number ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160663100 --- RELEASENOTES.md | 22 +++++++++++++++++-- constants.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 ++-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 ++--- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4f147e2bbd..f9f6b02c19 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,11 +1,29 @@ # Release notes # +### r2.4.3 ### + +* Audio: Workaround custom audio decoders misreporting their maximum supported + channel counts ([#2940](https://github.com/google/ExoPlayer/issues/2940)). +* Audio: Workaround for broken MediaTek raw decoder on some devices + ([#2873](https://github.com/google/ExoPlayer/issues/2873)). +* Captions: Fix TTML captions appearing at the top of the screen + ([#2953](https://github.com/google/ExoPlayer/issues/2953)). +* Captions: Fix handling of some DVB subtitles + ([#2957](https://github.com/google/ExoPlayer/issues/2957)). +* Track selection: Fix setSelectionOverride(index, tracks, null) + ([#2988](https://github.com/google/ExoPlayer/issues/2988)). +* GVR extension: Add support for mono input + ([#2710](https://github.com/google/ExoPlayer/issues/2710)). +* FLAC extension: Fix failing build + ([#2977](https://github.com/google/ExoPlayer/pull/2977)). +* Misc bugfixes. + ### r2.4.2 ### * Stability: Work around Nexus 10 reboot when playing certain content - ([2806](https://github.com/google/ExoPlayer/issues/2806)). + ([#2806](https://github.com/google/ExoPlayer/issues/2806)). * MP3: Correctly treat MP3s with INFO headers as constant bitrate - ([2895](https://github.com/google/ExoPlayer/issues/2895)). + ([#2895](https://github.com/google/ExoPlayer/issues/2895)). * HLS: Use average rather than peak bandwidth when available ([#2863](https://github.com/google/ExoPlayer/issues/2863)). * SmoothStreaming: Fix timeline for live streams diff --git a/constants.gradle b/constants.gradle index 95221a106f..df5cf900a1 100644 --- a/constants.gradle +++ b/constants.gradle @@ -24,7 +24,7 @@ project.ext { supportLibraryVersion = '25.3.1' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' - releaseVersion = 'r2.4.2' + releaseVersion = 'r2.4.3' modulePrefix = ':'; if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 34256d41c1..addce60cad 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2403" + android:versionName="2.4.3"> 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 c6fc139208..650ce727cd 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 @@ -24,13 +24,13 @@ public interface 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. - String VERSION = "2.4.2"; + String VERSION = "2.4.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. - String VERSION_SLASHY = "ExoPlayerLib/2.4.2"; + String VERSION_SLASHY = "ExoPlayerLib/2.4.3"; /** * The version of the library expressed as an integer, for example 1002003. @@ -40,7 +40,7 @@ public interface ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - int VERSION_INT = 2004002; + int VERSION_INT = 2004003; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 19a3d94022d4450eaccaaf48bcbf71f27aef4418 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 30 Jun 2017 22:24:40 +0100 Subject: [PATCH 211/220] Remove offline classes for now (not ready yet) --- .../exoplayer2/source/offline/Downloader.java | 195 ------------------ .../source/offline/DownloaderException.java | 28 --- .../source/offline/DownloaderFactory.java | 88 -------- 3 files changed, 311 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/offline/Downloader.java delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/offline/DownloaderException.java delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/offline/DownloaderFactory.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/offline/Downloader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/offline/Downloader.java deleted file mode 100644 index 04735551cd..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/offline/Downloader.java +++ /dev/null @@ -1,195 +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.source.offline; - -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.upstream.DataSink; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DummyDataSource; -import com.google.android.exoplayer2.upstream.FileDataSource; -import com.google.android.exoplayer2.upstream.PriorityDataSource; -import com.google.android.exoplayer2.upstream.cache.Cache; -import com.google.android.exoplayer2.upstream.cache.CacheDataSink; -import com.google.android.exoplayer2.upstream.cache.CacheDataSource; -import com.google.android.exoplayer2.util.ClosedSource; -import com.google.android.exoplayer2.util.PriorityTaskManager; -import java.io.IOException; - -/** - * Base class for stream downloaders. - * - *

        All of the methods are blocking. Also they are not thread safe, except {@link - * #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link #getDownloadedBytes()}. - * - * @param The type of the manifest object. - * @param The type of the representation key object. - */ -@ClosedSource(reason = "Not ready yet") -public abstract class Downloader { - - /** - * Listener notified when download progresses. - */ - public interface ProgressListener { - /** - * Called for the first time after the initialization and then after download of each segment. - * It is called on the thread which invoked {@link #downloadRepresentations(ProgressListener)}. - * - * @param downloader The reporting instance. - * @param totalSegments Total number of segments in the content. - * @param downloadedSegments Total number of downloaded segments. - * @param downloadedBytes Total number of downloaded bytes. - * @see #downloadRepresentations(ProgressListener) - */ - void onDownloadProgress(Downloader downloader, int totalSegments, - int downloadedSegments, long downloadedBytes); - } - - protected final Cache cache; - protected final CacheDataSource dataSource; - protected final CacheDataSource offlineDataSource; - protected final PriorityTaskManager priorityTaskManager; - protected final String manifestUri; - - protected volatile int totalSegments; - protected volatile int downloadedSegments; - protected volatile long downloadedBytes; - - /** - * Constructs a Downloader. - * - * @param manifestUri The URI of the manifest to be downloaded. - * @param cache Cache instance to be used to store downloaded data. - * @param upstreamDataSource A {@link DataSource} for downloading data. - * @param cacheReadDataSource A {@link DataSource} for reading data from the cache. - * If null, a {@link FileDataSource} instance is created and used. - * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If - * null, a {@link CacheDataSink} instance is created and used. - * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with - * download. Downloader priority is {@link C#PRIORITY_DOWNLOAD}. - */ - public Downloader(String manifestUri, Cache cache, DataSource upstreamDataSource, - @Nullable DataSource cacheReadDataSource, @Nullable DataSink cacheWriteDataSink, - @Nullable PriorityTaskManager priorityTaskManager) { - if (priorityTaskManager != null) { - upstreamDataSource = - new PriorityDataSource(upstreamDataSource, priorityTaskManager, C.PRIORITY_DOWNLOAD); - } else { - priorityTaskManager = new PriorityTaskManager(); // dummy PriorityTaskManager - } - if (cacheReadDataSource == null) { - cacheReadDataSource = new FileDataSource(); - } - if (cacheWriteDataSink == null) { - cacheWriteDataSink = new CacheDataSink(cache, - CacheDataSource.DEFAULT_MAX_CACHE_FILE_SIZE); - } - - this.manifestUri = manifestUri; - this.cache = cache; - this.dataSource = new CacheDataSource(cache, upstreamDataSource, cacheReadDataSource, - cacheWriteDataSink, CacheDataSource.FLAG_BLOCK_ON_CACHE, null); - this.offlineDataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, - cacheReadDataSource, null, CacheDataSource.FLAG_BLOCK_ON_CACHE, null); - this.priorityTaskManager = priorityTaskManager; - - resetCounters(); - } - - /** - * Downloads the manifest. - * - * @return The downloaded manifest. - * @throws IOException If an error occurs reading data from the stream. - */ - public abstract M downloadManifest() throws IOException; - - /** - * Selects multiple representations pointed to by the keys for downloading, removing or checking - * status. Any previous selection is cleared. - */ - public abstract void selectRepresentations(K... keys); - - /** - * Initializes the total segments, downloaded segments and downloaded bytes counters for the - * selected representations. - * - * @throws IOException Thrown when there is an error while reading from cache. - * @throws DownloaderException Thrown when a representation index is unbounded. - * @throws InterruptedException If the thread has been interrupted. - * @see #getTotalSegments() - * @see #getDownloadedSegments() - * @see #getDownloadedBytes() - */ - public abstract void initStatus() throws DownloaderException, InterruptedException, IOException; - - /** - * Downloads the content for the selected representations in sync or resumes a previously stopped - * download. - * - * @throws IOException Thrown when there is an error while downloading. - * @throws DownloaderException Thrown when no index data can be found for a representation or - * the index is unbounded. - * @throws InterruptedException If the thread has been interrupted. - */ - public abstract void downloadRepresentations(@Nullable ProgressListener listener) - throws IOException, DownloaderException, InterruptedException; - - /** - * Returns the total number of segments in the representations which are selected, or {@link - * C#LENGTH_UNSET} if it hasn't been calculated yet. - * - * @see #initStatus() - */ - public final int getTotalSegments() { - return totalSegments; - } - - /** - * Returns the total number of downloaded segments in the representations which are selected, or - * {@link C#LENGTH_UNSET} if it hasn't been calculated yet. - * - * @see #initStatus() - */ - public final int getDownloadedSegments() { - return downloadedSegments; - } - - /** - * Returns the total number of downloaded bytes in the representations which are selected, or - * {@link C#LENGTH_UNSET} if it hasn't been calculated yet. - * - * @see #initStatus() - */ - public final long getDownloadedBytes() { - return downloadedBytes; - } - - /** - * Removes all representations declared in the manifest and the manifest itself. - * - * @throws InterruptedException Thrown if the thread was interrupted. - */ - public abstract void removeAll() throws InterruptedException; - - protected final void resetCounters() { - totalSegments = C.LENGTH_UNSET; - downloadedSegments = C.LENGTH_UNSET; - downloadedBytes = C.LENGTH_UNSET; - } - -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/offline/DownloaderException.java b/library/core/src/main/java/com/google/android/exoplayer2/source/offline/DownloaderException.java deleted file mode 100644 index c07565b721..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/offline/DownloaderException.java +++ /dev/null @@ -1,28 +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.source.offline; - -import com.google.android.exoplayer2.util.ClosedSource; - -/** Thrown on an error in {@link Downloader}. */ -@ClosedSource(reason = "Not ready yet") -public final class DownloaderException extends Exception { - - public DownloaderException(String message) { - super(message); - } - -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/offline/DownloaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/offline/DownloaderFactory.java deleted file mode 100644 index 1bce12ce05..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/offline/DownloaderFactory.java +++ /dev/null @@ -1,88 +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.source.offline; - -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.upstream.DataSink; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DataSource.Factory; -import com.google.android.exoplayer2.upstream.cache.Cache; -import com.google.android.exoplayer2.util.ClosedSource; -import com.google.android.exoplayer2.util.PriorityTaskManager; - -/** - * A factory class that produces {@link Downloader}. - */ -@ClosedSource(reason = "Not ready yet") -public abstract class DownloaderFactory> { - - private final Factory upstreamDataSourceFactory; - private final Factory cacheReadDataSourceFactory; - private final DataSink.Factory cacheWriteDataSinkFactory; - - protected final Cache cache; - protected final PriorityTaskManager priorityTaskManager; - - /** - * Constructs a DashDownloaderFactory. - * - * @param cache Cache instance to be used to store downloaded data. - * @param upstreamDataSourceFactory A {@link Factory} for downloading data. - */ - public DownloaderFactory(Cache cache, Factory upstreamDataSourceFactory) { - this(cache, upstreamDataSourceFactory, null, null, null); - } - - /** - * Constructs a DashDownloaderFactory. - * - * @param cache Cache instance to be used to store downloaded data. - * @param upstreamDataSourceFactory A {@link Factory} for downloading data. - * @param cacheReadDataSourceFactory A {@link Factory} for reading data from the cache. - * If null, null is passed to {@link Downloader} constructor. - * @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for writing data to the cache. If - * null, null is passed to {@link Downloader} constructor. - * @param priorityTaskManager If one is given then the download priority is set lower than - * loading. If null, null is passed to {@link Downloader} constructor. - */ - public DownloaderFactory(Cache cache, Factory upstreamDataSourceFactory, - @Nullable Factory cacheReadDataSourceFactory, - @Nullable DataSink.Factory cacheWriteDataSinkFactory, - @Nullable PriorityTaskManager priorityTaskManager) { - this.cache = cache; - this.upstreamDataSourceFactory = upstreamDataSourceFactory; - this.cacheReadDataSourceFactory = cacheReadDataSourceFactory; - this.cacheWriteDataSinkFactory = cacheWriteDataSinkFactory; - this.priorityTaskManager = priorityTaskManager; - } - - /** - * Creates a {@link Downloader} with the given manifest. - * - * @param manifestUri The URI of the manifest of the DASH to be downloaded. - * @return A {@link Downloader}. - */ - public final T create(String manifestUri) { - return create(manifestUri, - upstreamDataSourceFactory != null ? upstreamDataSourceFactory.createDataSource() : null, - cacheReadDataSourceFactory != null ? cacheReadDataSourceFactory.createDataSource() : null, - cacheWriteDataSinkFactory != null ? cacheWriteDataSinkFactory.createDataSink() : null); - } - - protected abstract T create(String manifestUri, DataSource upstreamDataSource, - DataSource cacheReadDataSource, DataSink cacheWriteDataSink); - -} From a9efb4553d978fc89e2b0b8a3cc96dbe34f9a8d9 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 3 Jul 2017 02:07:39 -0700 Subject: [PATCH 212/220] Merge TimelineAsserts.FakeTimeline into FakeTimeline. They serve the same purpose. One was defined as single window, multi-period timeline, while the other was a multi-window, single-period-each timeline. The combined FakeTimeline uses TimelineWindowDefinitions which allow multi- window, multi-period fake timelines. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160808844 --- .../android/exoplayer2/TimelineTest.java | 7 ++-- .../source/ClippingMediaSourceTest.java | 6 ++- .../source/ConcatenatingMediaSourceTest.java | 18 ++++---- .../DynamicConcatenatingMediaSourceTest.java | 21 ++++++---- .../source/LoopingMediaSourceTest.java | 11 +++-- .../exoplayer2/testutil/FakeTimeline.java | 41 ++++++++++++++---- .../exoplayer2/testutil/TimelineAsserts.java | 42 ------------------- 7 files changed, 70 insertions(+), 76 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java index bf6ee31165..d69f40283f 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java @@ -15,8 +15,9 @@ */ package com.google.android.exoplayer2; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.TimelineAsserts; -import com.google.android.exoplayer2.testutil.TimelineAsserts.FakeTimeline; import junit.framework.TestCase; /** @@ -29,7 +30,7 @@ public class TimelineTest extends TestCase { } public void testSinglePeriodTimeline() { - Timeline timeline = new FakeTimeline(1, 111); + Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(1, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 1); TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); @@ -41,7 +42,7 @@ public class TimelineTest extends TestCase { } public void testMultiPeriodTimeline() { - Timeline timeline = new FakeTimeline(5, 111); + Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(5, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 5); TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index a7b37e2e23..f14ee088bc 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -22,8 +22,9 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.TimelineAsserts; -import com.google.android.exoplayer2.testutil.TimelineAsserts.FakeTimeline; /** * Unit tests for {@link ClippingMediaSource}. @@ -101,7 +102,8 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testWindowAndPeriodIndices() { - Timeline timeline = new FakeTimeline(1, 111); + Timeline timeline = new FakeTimeline( + new TimelineWindowDefinition(1, 111, true, false, TEST_PERIOD_DURATION_US)); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); TimelineAsserts.assertWindowIds(clippedTimeline, 111); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 40551aa38f..07a9324c52 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -19,8 +19,9 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.TimelineAsserts; -import com.google.android.exoplayer2.testutil.TimelineAsserts.FakeTimeline; import junit.framework.TestCase; /** @@ -29,7 +30,7 @@ import junit.framework.TestCase; public final class ConcatenatingMediaSourceTest extends TestCase { public void testSingleMediaSource() { - Timeline timeline = getConcatenatedTimeline(false, new FakeTimeline(3, 111)); + Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); @@ -39,7 +40,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0); TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0); - timeline = getConcatenatedTimeline(true, new FakeTimeline(3, 111)); + timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); @@ -51,8 +52,8 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } public void testMultipleMediaSources() { - Timeline[] timelines = { new FakeTimeline(3, 111), new FakeTimeline(1, 222), - new FakeTimeline(3, 333) }; + Timeline[] timelines = { createFakeTimeline(3, 111), createFakeTimeline(1, 222), + createFakeTimeline(3, 333) }; Timeline timeline = getConcatenatedTimeline(false, timelines); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); @@ -80,8 +81,8 @@ public final class ConcatenatingMediaSourceTest extends TestCase { public void testNestedMediaSources() { Timeline timeline = getConcatenatedTimeline(false, - getConcatenatedTimeline(false, new FakeTimeline(1, 111), new FakeTimeline(1, 222)), - getConcatenatedTimeline(true, new FakeTimeline(1, 333), new FakeTimeline(1, 444))); + getConcatenatedTimeline(false, createFakeTimeline(1, 111), createFakeTimeline(1, 222)), + getConcatenatedTimeline(true, createFakeTimeline(1, 333), createFakeTimeline(1, 444))); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1); TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, @@ -108,5 +109,8 @@ public final class ConcatenatingMediaSourceTest extends TestCase { new ConcatenatingMediaSource(isRepeatOneAtomic, mediaSources)); } + private static FakeTimeline createFakeTimeline(int periodCount, int windowId) { + return new FakeTimeline(new TimelineWindowDefinition(periodCount, windowId)); + } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 982d37c3a1..520d99892a 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -26,8 +26,9 @@ import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.TimelineAsserts; -import com.google.android.exoplayer2.testutil.TimelineAsserts.FakeTimeline; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; @@ -175,7 +176,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertWindowIds(timeline, 111, null); TimelineAsserts.assertWindowIsDynamic(timeline, false, true); - lazySources[1].triggerTimelineUpdate(new FakeTimeline(9, 999)); + lazySources[1].triggerTimelineUpdate(createFakeTimeline(8)); waitForTimelineUpdate(); TimelineAsserts.assertPeriodCounts(timeline, 1, 9); TimelineAsserts.assertWindowIds(timeline, 111, 999); @@ -194,7 +195,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999); TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false); - lazySources[3].triggerTimelineUpdate(new FakeTimeline(8, 888)); + lazySources[3].triggerTimelineUpdate(createFakeTimeline(7)); waitForTimelineUpdate(); TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999); @@ -207,7 +208,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { public void testIllegalArguments() { DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); - MediaSource validSource = new FakeMediaSource(new FakeTimeline(1, 1), null); + MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null); // Null sources. try { @@ -235,9 +236,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } mediaSources = new MediaSource[] { - new FakeMediaSource(new FakeTimeline(1, 1), null), - validSource - }; + new FakeMediaSource(createFakeTimeline(2), null), validSource }; try { mediaSource.addMediaSources(Arrays.asList(mediaSources)); fail("Duplicate mediaSource not allowed."); @@ -270,14 +269,18 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { timelineUpdated = false; } - private FakeMediaSource[] createMediaSources(int count) { + private static FakeMediaSource[] createMediaSources(int count) { FakeMediaSource[] sources = new FakeMediaSource[count]; for (int i = 0; i < count; i++) { - sources[i] = new FakeMediaSource(new FakeTimeline(i + 1, (i + 1) * 111), null); + sources[i] = new FakeMediaSource(createFakeTimeline(i), null); } return sources; } + private static FakeTimeline createFakeTimeline(int index) { + return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111)); + } + private static class LazyMediaSource implements MediaSource { private Listener listener; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index 6157487005..5c1898ac7d 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -19,8 +19,9 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.TimelineAsserts; -import com.google.android.exoplayer2.testutil.TimelineAsserts.FakeTimeline; import junit.framework.TestCase; /** @@ -31,11 +32,9 @@ public class LoopingMediaSourceTest extends TestCase { private final Timeline multiWindowTimeline; public LoopingMediaSourceTest() { - multiWindowTimeline = TimelineAsserts.extractTimelineFromMediaSource( - new ConcatenatingMediaSource( - new FakeMediaSource(new FakeTimeline(1, 111), null), - new FakeMediaSource(new FakeTimeline(1, 222), null), - new FakeMediaSource(new FakeTimeline(1, 333), null))); + multiWindowTimeline = TimelineAsserts.extractTimelineFromMediaSource(new FakeMediaSource( + new FakeTimeline(new TimelineWindowDefinition(1, 111), + new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333)), null)); } public void testSingleLoop() { 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 0b18b00adc..040782264b 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.testutil; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Util; /** * Fake {@link Timeline} which can be setup to return custom {@link TimelineWindowDefinition}s. @@ -28,11 +29,26 @@ public final class FakeTimeline extends Timeline { */ public static final class TimelineWindowDefinition { + private static final int WINDOW_DURATION_US = 100000; + + public final int periodCount; + public final Object id; public final boolean isSeekable; public final boolean isDynamic; public final long durationUs; + public TimelineWindowDefinition(int periodCount, Object id) { + this(periodCount, id, true, false, WINDOW_DURATION_US); + } + public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) { + this(1, 0, isSeekable, isDynamic, durationUs); + } + + public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, + boolean isDynamic, long durationUs) { + this.periodCount = periodCount; + this.id = id; this.isSeekable = isSeekable; this.isDynamic = isDynamic; this.durationUs = durationUs; @@ -41,9 +57,15 @@ public final class FakeTimeline extends Timeline { } private final TimelineWindowDefinition[] windowDefinitions; + private final int[] periodOffsets; public FakeTimeline(TimelineWindowDefinition... windowDefinitions) { this.windowDefinitions = windowDefinitions; + periodOffsets = new int[windowDefinitions.length + 1]; + periodOffsets[0] = 0; + for (int i = 0; i < windowDefinitions.length; i++) { + periodOffsets[i + 1] = periodOffsets[i] + windowDefinitions[i].periodCount; + } } @Override @@ -55,21 +77,26 @@ public final class FakeTimeline extends Timeline { public Window getWindow(int windowIndex, Window window, boolean setIds, long defaultPositionProjectionUs) { TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; - Object id = setIds ? windowIndex : null; + Object id = setIds ? windowDefinition.id : null; return window.set(id, C.TIME_UNSET, C.TIME_UNSET, windowDefinition.isSeekable, - windowDefinition.isDynamic, 0, windowDefinition.durationUs, windowIndex, windowIndex, 0); + windowDefinition.isDynamic, 0, windowDefinition.durationUs, periodOffsets[windowIndex], + periodOffsets[windowIndex + 1] - 1, 0); } @Override public int getPeriodCount() { - return windowDefinitions.length; + return periodOffsets[periodOffsets.length - 1]; } @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { - TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex]; - Object id = setIds ? periodIndex : null; - return period.set(id, id, periodIndex, windowDefinition.durationUs, 0); + int windowIndex = Util.binarySearchFloor(periodOffsets, periodIndex, true, false); + int windowPeriodIndex = periodIndex - periodOffsets[windowIndex]; + TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; + Object id = setIds ? windowPeriodIndex : null; + Object uid = setIds ? periodIndex : null; + long periodDurationUs = windowDefinition.durationUs / windowDefinition.periodCount; + return period.set(id, uid, windowIndex, periodDurationUs, periodDurationUs * windowPeriodIndex); } @Override @@ -78,7 +105,7 @@ public final class FakeTimeline extends Timeline { return C.INDEX_UNSET; } int index = (Integer) uid; - return index >= 0 && index < windowDefinitions.length ? index : C.INDEX_UNSET; + return index >= 0 && index < getPeriodCount() ? index : C.INDEX_UNSET; } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index 97fd9b07ea..029a303a33 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -32,48 +32,6 @@ public final class TimelineAsserts { private TimelineAsserts() {} - /** - * Fake timeline with multiple periods and user-defined window id. - */ - public static final class FakeTimeline extends Timeline { - - private static final int WINDOW_DURATION_US = 1000000; - - private final int periodCount; - private final int id; - - public FakeTimeline(int periodCount, int id) { - this.periodCount = periodCount; - this.id = id; - } - - @Override - public int getWindowCount() { - return 1; - } - - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - return window.set(id, 0, 0, true, false, 0, WINDOW_DURATION_US, 0, periodCount - 1, 0); - } - - @Override - public int getPeriodCount() { - return periodCount; - } - - @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - return period.set(periodIndex, null, 0, WINDOW_DURATION_US, 0); - } - - @Override - public int getIndexOfPeriod(Object uid) { - return C.INDEX_UNSET; - } - } - /** * Extracts the timeline from a media source. */ From a82e51070b0e26466c34463c900f7d2a6b848b02 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 3 Jul 2017 06:45:25 -0700 Subject: [PATCH 213/220] Add playback tests for CENC/DASH streams. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160825705 --- .../gts/CommonEncryptionDrmTest.java | 96 +++++++++++++++++++ .../playbacktests/gts/DashStreamingTest.java | 30 +++--- .../playbacktests/gts/DashTestData.java | 17 ++-- .../playbacktests/gts/DashTestRunner.java | 5 +- .../gts/DashWidevineOfflineTest.java | 4 +- 5 files changed, 127 insertions(+), 25 deletions(-) create mode 100644 playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java new file mode 100644 index 0000000000..3f84b9ea85 --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java @@ -0,0 +1,96 @@ +/* + * 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.playbacktests.gts; + +import android.test.ActivityInstrumentationTestCase2; +import com.google.android.exoplayer2.testutil.ActionSchedule; +import com.google.android.exoplayer2.testutil.HostActivity; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; + +/** + * Test playback of encrypted DASH streams using different CENC scheme types. + */ +public final class CommonEncryptionDrmTest extends ActivityInstrumentationTestCase2 { + + private static final String TAG = "CencDrmTest"; + + private static final String URL_cenc = + "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd"; + private static final String URL_cbc1 = + "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd"; + private static final String URL_cbcs = + "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd"; + private static final String ID_AUDIO = "0"; + private static final String[] IDS_VIDEO = new String[] {"1", "2"}; + + // Seeks help reproduce playback issues in certain devices. + private static final ActionSchedule ACTION_SCHEDULE_WITH_SEEKS = new ActionSchedule.Builder(TAG) + .delay(30000).seek(300000).delay(10000).seek(270000).delay(10000).seek(200000).delay(10000) + .stop().build(); + + private DashTestRunner testRunner; + + public CommonEncryptionDrmTest() { + super(HostActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + testRunner = new DashTestRunner(TAG, getActivity(), getInstrumentation()) + .setWidevineInfo(MimeTypes.VIDEO_H264, false) + .setActionSchedule(ACTION_SCHEDULE_WITH_SEEKS) + .setAudioVideoFormats(ID_AUDIO, IDS_VIDEO) + .setCanIncludeAdditionalVideoFormats(true); + } + + @Override + protected void tearDown() throws Exception { + testRunner = null; + super.tearDown(); + } + + public void testCencSchemeType() { + if (Util.SDK_INT < 18) { + // Pass. + return; + } + testRunner.setStreamName("test_widevine_h264_scheme_cenc").setManifestUrl(URL_cenc).run(); + } + + public void testCbc1SchemeType() { + if (Util.SDK_INT < 24) { + // Pass. + return; + } + testRunner.setStreamName("test_widevine_h264_scheme_cbc1").setManifestUrl(URL_cbc1).run(); + } + + public void testCbcsSchemeType() { + if (Util.SDK_INT < 24) { + // Pass. + return; + } + testRunner.setStreamName("test_widevine_h264_scheme_cbcs").setManifestUrl(URL_cbcs).run(); + } + + public void testCensSchemeType() { + // TODO: Implement once content is available. Track [internal: b/31219813]. + } + +} diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java index 669241e65c..529f57582e 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java @@ -345,7 +345,7 @@ public final class DashStreamingTest extends ActivityInstrumentationTestCase2 Date: Mon, 3 Jul 2017 07:10:21 -0700 Subject: [PATCH 214/220] Move extractTimelineFromMediaSource to test util class. This also ensures that TimelineAsserts only contains assert methods. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160827271 --- .../source/ClippingMediaSourceTest.java | 3 +- .../source/ConcatenatingMediaSourceTest.java | 3 +- .../source/LoopingMediaSourceTest.java | 5 ++-- .../android/exoplayer2/testutil/TestUtil.java | 29 +++++++++++++++++++ .../exoplayer2/testutil/TimelineAsserts.java | 18 ------------ 5 files changed, 36 insertions(+), 22 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index f14ee088bc..1a15b750ac 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; /** @@ -123,7 +124,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { */ private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { MediaSource mediaSource = new FakeMediaSource(timeline, null); - return TimelineAsserts.extractTimelineFromMediaSource( + return TestUtil.extractTimelineFromMediaSource( new ClippingMediaSource(mediaSource, startMs, endMs)); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 07a9324c52..49f34f7b2b 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import junit.framework.TestCase; @@ -105,7 +106,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { for (int i = 0; i < timelines.length; i++) { mediaSources[i] = new FakeMediaSource(timelines[i], null); } - return TimelineAsserts.extractTimelineFromMediaSource( + return TestUtil.extractTimelineFromMediaSource( new ConcatenatingMediaSource(isRepeatOneAtomic, mediaSources)); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index 5c1898ac7d..87e6bb9983 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import junit.framework.TestCase; @@ -32,7 +33,7 @@ public class LoopingMediaSourceTest extends TestCase { private final Timeline multiWindowTimeline; public LoopingMediaSourceTest() { - multiWindowTimeline = TimelineAsserts.extractTimelineFromMediaSource(new FakeMediaSource( + multiWindowTimeline = TestUtil.extractTimelineFromMediaSource(new FakeMediaSource( new FakeTimeline(new TimelineWindowDefinition(1, 111), new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333)), null)); } @@ -87,7 +88,7 @@ public class LoopingMediaSourceTest extends TestCase { */ private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) { MediaSource mediaSource = new FakeMediaSource(timeline, null); - return TimelineAsserts.extractTimelineFromMediaSource( + return TestUtil.extractTimelineFromMediaSource( new LoopingMediaSource(mediaSource, loopCount)); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 363f60b10d..9a26c415f4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -19,9 +19,12 @@ import android.app.Instrumentation; import android.test.InstrumentationTestCase; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -218,6 +221,32 @@ public class TestUtil { return new FakeExtractorInput.Builder().setData(data).build(); } + /** + * Extracts the timeline from a media source. + */ + public static Timeline extractTimelineFromMediaSource(MediaSource mediaSource) { + class TimelineListener implements Listener { + private Timeline timeline; + @Override + public synchronized void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + this.timeline = timeline; + this.notify(); + } + } + TimelineListener listener = new TimelineListener(); + mediaSource.prepareSource(null, true, listener); + synchronized (listener) { + while (listener.timeline == null) { + try { + listener.wait(); + } catch (InterruptedException e) { + Assert.fail(e.getMessage()); + } + } + } + return listener.timeline; + } + /** * Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, * boolean)} with all possible combinations of "simulate" parameters. diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index 029a303a33..afbfbb59db 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -22,8 +22,6 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSource.Listener; /** * Unit test for {@link Timeline}. @@ -32,22 +30,6 @@ public final class TimelineAsserts { private TimelineAsserts() {} - /** - * Extracts the timeline from a media source. - */ - public static Timeline extractTimelineFromMediaSource(MediaSource mediaSource) { - class TimelineListener implements Listener { - private Timeline timeline; - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - this.timeline = timeline; - } - } - TimelineListener listener = new TimelineListener(); - mediaSource.prepareSource(null, true, listener); - return listener.timeline; - } - /** * Assert that timeline is empty (i.e. has no windows or periods). */ From d733bb4101330dcb7fb4ab7d3047299081bf577d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 3 Jul 2017 07:14:56 -0700 Subject: [PATCH 215/220] Fix video tunneling state transition to ready Issue: #2985 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160827532 --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 5ef865b817..8878cf2e73 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 @@ -268,7 +268,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override public boolean isReady() { if (super.isReady() && (renderedFirstFrame || (dummySurface != null && surface == dummySurface) - || getCodec() == null)) { + || getCodec() == null || tunneling)) { // Ready. If we were joining then we've now joined, so clear the joining deadline. joiningDeadlineMs = C.TIME_UNSET; return true; From ad3d1e0cf2ea085d18627184a5213f47982b738a Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 3 Jul 2017 07:23:03 -0700 Subject: [PATCH 216/220] Add reset() to SampleQueue. Deprecate reset(boolean) and disable() The deprecated methods will be removed as soon as HLS is migrated to use the new ones. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160827936 --- .../exoplayer2/source/SampleQueueTest.java | 20 +++++++++---------- .../source/ExtractorMediaPeriod.java | 20 +++++++++++++------ .../exoplayer2/source/SampleQueue.java | 13 +++++++++--- .../source/chunk/ChunkSampleStream.java | 12 +++++------ 4 files changed, 40 insertions(+), 25 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 89a3db3599..76ea0e34cf 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -93,10 +93,10 @@ public class SampleQueueTest extends TestCase { inputBuffer = null; } - public void testDisableReleasesAllocations() { + public void testResetReleasesAllocations() { writeTestData(); assertAllocationCount(10); - sampleQueue.disable(); + sampleQueue.reset(); assertAllocationCount(0); } @@ -545,8 +545,8 @@ public class SampleQueueTest extends TestCase { } /** - * Asserts {@link SampleQueue#readData} is behaving correctly, given there are no samples - * to read and the last format to be written to the sample queue is {@code endFormat}. + * Asserts {@link SampleQueue#read} is behaving correctly, given there are no samples to read and + * the last format to be written to the sample queue is {@code endFormat}. * * @param endFormat The last format to be written to the sample queue, or null of no format has * been written. @@ -573,7 +573,7 @@ public class SampleQueueTest extends TestCase { } /** - * Asserts {@link SampleQueue#readData} returns {@link C#RESULT_NOTHING_READ}. + * Asserts {@link SampleQueue#read} returns {@link C#RESULT_NOTHING_READ}. * * @param formatRequired The value of {@code formatRequired} passed to readData. */ @@ -589,7 +589,7 @@ public class SampleQueueTest extends TestCase { } /** - * Asserts {@link SampleQueue#readData} returns {@link C#RESULT_BUFFER_READ} and that the + * 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. @@ -608,8 +608,8 @@ public class SampleQueueTest extends TestCase { } /** - * Asserts {@link SampleQueue#readData} returns {@link C#RESULT_FORMAT_READ} and that the - * format holder is filled with a {@link Format} that equals {@code format}. + * 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 format The expected format. @@ -626,8 +626,8 @@ public class SampleQueueTest extends TestCase { } /** - * Asserts {@link SampleQueue#readData} returns {@link C#RESULT_BUFFER_READ} and that the - * buffer is filled with the specified sample data. + * Asserts {@link SampleQueue#read} returns {@link C#RESULT_BUFFER_READ} and that the buffer is + * filled with the specified sample data. * * @param timeUs The expected buffer timestamp. * @param isKeyframe The expected keyframe flag. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 189391f711..79bc753241 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -150,8 +150,8 @@ import java.util.Arrays; public void release() { boolean releasedSynchronously = loader.release(this); if (prepared && !releasedSynchronously) { - // Discard as much as we can synchronously. We only do this if we're prepared, since - // otherwise sampleQueues may still be being modified by the loading thread. + // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise + // sampleQueues may still be being modified by the loading thread. for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.discardToEnd(); } @@ -164,7 +164,7 @@ import java.util.Arrays; public void onLoaderReleased() { extractorHolder.release(); for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.reset(true); + sampleQueue.reset(); } } @@ -227,7 +227,15 @@ import java.util.Arrays; if (enabledTrackCount == 0) { notifyReset = false; if (loader.isLoading()) { + // Discard as much as we can synchronously. + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.discardToEnd(); + } loader.cancelLoading(); + } else { + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(); + } } } else if (seekRequired) { positionUs = seekToUs(positionUs); @@ -327,7 +335,7 @@ import java.util.Arrays; loader.cancelLoading(); } else { for (int i = 0; i < trackCount; i++) { - sampleQueues[i].reset(trackEnabledStates[i]); + sampleQueues[i].reset(); } } } @@ -388,7 +396,7 @@ import java.util.Arrays; } copyLengthFromLoader(loadable); for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.reset(true); + sampleQueue.reset(); } if (enabledTrackCount > 0) { callback.onContinueLoadingRequested(this); @@ -526,7 +534,7 @@ import java.util.Arrays; lastSeekPositionUs = 0; notifyReset = prepared; for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.reset(true); + sampleQueue.reset(); } loadable.setLoadPosition(0, 0); } 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 ad906bfe9d..1b70b03a29 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 @@ -98,9 +98,15 @@ public final class SampleQueue implements TrackOutput { /** * Resets the output. - * - * @param enable Whether the output should be enabled. False if it should be disabled. */ + public void reset() { + reset(true); + } + + /** + * @deprecated Use {@link #reset()}. Don't disable sample queues. + */ + @Deprecated public void reset(boolean enable) { int previousState = state.getAndSet(enable ? STATE_ENABLED : STATE_DISABLED); clearSampleData(); @@ -169,8 +175,9 @@ public final class SampleQueue implements TrackOutput { // Called by the consuming thread. /** - * Disables buffering of sample data and metadata. + * @deprecated Don't disable sample queues. */ + @Deprecated public void disable() { if (state.getAndSet(STATE_DISABLED) == STATE_ENABLED) { clearSampleData(); 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 f4bdbc1676..0fc3d5881e 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 @@ -203,9 +203,9 @@ public class ChunkSampleStream implements SampleStream, S if (loader.isLoading()) { loader.cancelLoading(); } else { - primarySampleQueue.reset(true); + primarySampleQueue.reset(); for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.reset(true); + embeddedSampleQueue.reset(); } } } @@ -229,9 +229,9 @@ public class ChunkSampleStream implements SampleStream, S @Override public void onLoaderReleased() { - primarySampleQueue.reset(true); + primarySampleQueue.reset(); for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.reset(true); + embeddedSampleQueue.reset(); } } @@ -295,9 +295,9 @@ public class ChunkSampleStream implements SampleStream, S loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); if (!released) { - primarySampleQueue.reset(true); + primarySampleQueue.reset(); for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.reset(true); + embeddedSampleQueue.reset(); } callback.onContinueLoadingRequested(this); } From 05a77eef5d24e307113787f67d30d8722032708d Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 3 Jul 2017 07:42:57 -0700 Subject: [PATCH 217/220] Move Extractor test assertion methods to ExtractorAsserts class. This cleans up test the TestUtil class that in large parts consisted of assertions for Extractor tests. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160829066 --- .../ext/flac/FlacExtractorTest.java | 5 +- .../extractor/flv/FlvExtractorTest.java | 5 +- .../extractor/mkv/MatroskaExtractorTest.java | 9 +- .../extractor/mp3/Mp3ExtractorTest.java | 7 +- .../mp4/FragmentedMp4ExtractorTest.java | 17 +- .../extractor/mp4/Mp4ExtractorTest.java | 5 +- .../extractor/ogg/OggExtractorTest.java | 12 +- .../extractor/rawcc/RawCcExtractorTest.java | 7 +- .../extractor/ts/Ac3ExtractorTest.java | 5 +- .../extractor/ts/AdtsExtractorTest.java | 5 +- .../extractor/ts/PsExtractorTest.java | 5 +- .../extractor/ts/TsExtractorTest.java | 6 +- .../extractor/wav/WavExtractorTest.java | 5 +- .../exoplayer2/testutil/ExtractorAsserts.java | 272 ++++++++++++++++++ .../android/exoplayer2/testutil/TestUtil.java | 252 ---------------- 15 files changed, 327 insertions(+), 290 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java 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 4196f1ea63..5954985100 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 @@ -17,7 +17,8 @@ package com.google.android.exoplayer2.ext.flac; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Unit test for {@link FlacExtractor}. @@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public class FlacExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertOutput(new ExtractorFactory() { @Override public Extractor create() { return new FlacExtractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java index 321181621e..4587c98317 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java @@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.flv; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Unit test for {@link FlvExtractor}. @@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class FlvExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertOutput(new ExtractorFactory() { @Override public Extractor create() { return new FlvExtractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java index 48eee69b50..57beec3ac6 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java @@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.mkv; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Tests for {@link MatroskaExtractor}. @@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class MatroskaExtractorTest extends InstrumentationTestCase { public void testMkvSample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertOutput(new ExtractorFactory() { @Override public Extractor create() { return new MatroskaExtractor(); @@ -34,7 +35,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase { } public void testWebmSubsampleEncryption() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertOutput(new ExtractorFactory() { @Override public Extractor create() { return new MatroskaExtractor(); @@ -43,7 +44,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase { } public void testWebmSubsampleEncryptionWithAltrefFrames() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertOutput(new ExtractorFactory() { @Override public Extractor create() { return new MatroskaExtractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java index c70710f1ee..3ad6a74bc9 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java @@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.mp3; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Unit test for {@link Mp3Extractor}. @@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class Mp3ExtractorTest extends InstrumentationTestCase { public void testMp3Sample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertOutput(new ExtractorFactory() { @Override public Extractor create() { return new Mp3Extractor(); @@ -34,7 +35,7 @@ public final class Mp3ExtractorTest extends InstrumentationTestCase { } public void testTrimmedMp3Sample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertOutput(new ExtractorFactory() { @Override public Extractor create() { return new Mp3Extractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java index 95ad8b446e..d8da8760e4 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java @@ -18,7 +18,8 @@ package com.google.android.exoplayer2.extractor.mp4; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Unit test for {@link FragmentedMp4Extractor}. @@ -26,26 +27,28 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - TestUtil.assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation()); + ExtractorAsserts + .assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation()); } public void testSampleWithSeiPayloadParsing() throws Exception { // Enabling the CEA-608 track enables SEI payload parsing. - TestUtil.assertOutput(getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK), + ExtractorAsserts.assertOutput( + getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK), "mp4/sample_fragmented_sei.mp4", getInstrumentation()); } public void testAtomWithZeroSize() throws Exception { - TestUtil.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4", + ExtractorAsserts.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4", getInstrumentation(), ParserException.class); } - private static TestUtil.ExtractorFactory getExtractorFactory() { + private static ExtractorFactory getExtractorFactory() { return getExtractorFactory(0); } - private static TestUtil.ExtractorFactory getExtractorFactory(final int flags) { - return new TestUtil.ExtractorFactory() { + private static ExtractorFactory getExtractorFactory(final int flags) { + return new ExtractorFactory() { @Override public Extractor create() { return new FragmentedMp4Extractor(flags, null); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java index 6ad777da70..a534d6dd24 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java @@ -18,7 +18,8 @@ package com.google.android.exoplayer2.extractor.mp4; import android.annotation.TargetApi; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Tests for {@link Mp4Extractor}. @@ -27,7 +28,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class Mp4ExtractorTest extends InstrumentationTestCase { public void testMp4Sample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertOutput(new ExtractorFactory() { @Override public Extractor create() { return new Mp4Extractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java index 04a6131652..26b7991869 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java @@ -17,9 +17,10 @@ package com.google.android.exoplayer2.extractor.ogg; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.TestUtil; -import com.google.android.exoplayer2.testutil.TestUtil.ExtractorFactory; import java.io.IOException; /** @@ -35,20 +36,21 @@ public final class OggExtractorTest extends InstrumentationTestCase { }; public void testOpus() throws Exception { - TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus", getInstrumentation()); + ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus", getInstrumentation()); } public void testFlac() throws Exception { - TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg", getInstrumentation()); + ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg", getInstrumentation()); } public void testFlacNoSeektable() throws Exception { - TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg", + ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg", getInstrumentation()); } public void testVorbis() throws Exception { - TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg", getInstrumentation()); + ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg", + getInstrumentation()); } public void testSniffVorbis() throws Exception { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java index 4e99e2745e..5a9d60512c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java @@ -19,7 +19,8 @@ import android.annotation.TargetApi; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import com.google.android.exoplayer2.util.MimeTypes; /** @@ -29,8 +30,8 @@ import com.google.android.exoplayer2.util.MimeTypes; public final class RawCcExtractorTest extends InstrumentationTestCase { public void testRawCcSample() throws Exception { - TestUtil.assertOutput( - new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertOutput( + new ExtractorFactory() { @Override public Extractor create() { return new RawCcExtractor( diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java index ab44e3aed3..1c18e44373 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java @@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Unit test for {@link Ac3Extractor}. @@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class Ac3ExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertOutput(new ExtractorFactory() { @Override public Extractor create() { return new Ac3Extractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java index e30a863d07..bc05be6fa8 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java @@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Unit test for {@link AdtsExtractor}. @@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class AdtsExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertOutput(new ExtractorFactory() { @Override public Extractor create() { return new AdtsExtractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java index ef97bef0ff..e6937ccbc8 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java @@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Unit test for {@link PsExtractor}. @@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class PsExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertOutput(new ExtractorFactory() { @Override public Extractor create() { return new PsExtractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index efd653b8d9..09c9facab0 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -25,6 +25,8 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput; @@ -43,7 +45,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. public void testSample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertOutput(new ExtractorFactory() { @Override public Extractor create() { return new TsExtractor(); @@ -65,7 +67,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1); fileData = out.toByteArray(); - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertOutput(new ExtractorFactory() { @Override public Extractor create() { return new TsExtractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java index a416d644b7..7c969fd386 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java @@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.wav; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Unit test for {@link WavExtractor}. @@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class WavExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertOutput(new ExtractorFactory() { @Override public Extractor create() { return new WavExtractor(); 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 new file mode 100644 index 0000000000..fb78b3a634 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java @@ -0,0 +1,272 @@ +/* + * 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.testutil; + +import android.app.Instrumentation; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.Arrays; +import junit.framework.Assert; + +/** + * Assertion methods for {@link Extractor}. + */ +public final class ExtractorAsserts { + + /** + * A factory for {@link Extractor} instances. + */ + public interface ExtractorFactory { + Extractor create(); + } + + private static final String DUMP_EXTENSION = ".dump"; + private static final String UNKNOWN_LENGTH_EXTENSION = ".unklen" + DUMP_EXTENSION; + + /** + * Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, + * boolean)} with all possible combinations of "simulate" parameters. + * + * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} + * class which is to be tested. + * @param sampleFile The path to the input sample. + * @param instrumentation To be used to load the sample file. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + * @see #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, boolean) + */ + public static void assertOutput(ExtractorFactory factory, String sampleFile, + Instrumentation instrumentation) throws IOException, InterruptedException { + byte[] fileData = TestUtil.getByteArray(instrumentation, sampleFile); + assertOutput(factory, sampleFile, fileData, instrumentation); + } + + /** + * Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, + * boolean)} with all possible combinations of "simulate" parameters. + * + * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} + * class which is to be tested. + * @param sampleFile The path to the input sample. + * @param fileData Content of the input file. + * @param instrumentation To be used to load the sample file. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + * @see #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, boolean) + */ + public static void assertOutput(ExtractorFactory factory, String sampleFile, byte[] fileData, + Instrumentation instrumentation) throws IOException, InterruptedException { + assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, false, false); + assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, false, false); + assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, true, false); + assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, true, false); + assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, false, true); + assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, false, true); + assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, true, true); + assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, true, true); + } + + /** + * Asserts that {@code extractor} consumes {@code sampleFile} successfully and its output equals + * to a prerecorded output dump file with the name {@code sampleFile} + "{@value + * #DUMP_EXTENSION}". If {@code simulateUnknownLength} is true and {@code sampleFile} + "{@value + * #UNKNOWN_LENGTH_EXTENSION}" exists, it's preferred. + * + * @param extractor The {@link Extractor} to be tested. + * @param sampleFile The path to the input sample. + * @param fileData Content of the input file. + * @param instrumentation To be used to load the sample file. + * @param simulateIOErrors If true simulates IOErrors. + * @param simulateUnknownLength If true simulates unknown input length. + * @param simulatePartialReads If true simulates partial reads. + * @return The {@link FakeExtractorOutput} used in the test. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + */ + public static FakeExtractorOutput assertOutput(Extractor extractor, String sampleFile, + byte[] fileData, Instrumentation instrumentation, boolean simulateIOErrors, + boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException, + InterruptedException { + FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData) + .setSimulateIOErrors(simulateIOErrors) + .setSimulateUnknownLength(simulateUnknownLength) + .setSimulatePartialReads(simulatePartialReads).build(); + + Assert.assertTrue(TestUtil.sniffTestData(extractor, input)); + input.resetPeekPosition(); + FakeExtractorOutput extractorOutput = consumeTestData(extractor, input, 0, true); + + if (simulateUnknownLength + && assetExists(instrumentation, sampleFile + UNKNOWN_LENGTH_EXTENSION)) { + extractorOutput.assertOutput(instrumentation, sampleFile + UNKNOWN_LENGTH_EXTENSION); + } else { + extractorOutput.assertOutput(instrumentation, sampleFile + ".0" + DUMP_EXTENSION); + } + + SeekMap seekMap = extractorOutput.seekMap; + if (seekMap.isSeekable()) { + long durationUs = seekMap.getDurationUs(); + for (int j = 0; j < 4; j++) { + long timeUs = (durationUs * j) / 3; + long position = seekMap.getPosition(timeUs); + 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(instrumentation, sampleFile + '.' + j + DUMP_EXTENSION); + } + } + + return extractorOutput; + } + + /** + * Calls {@link #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)} with all + * possible combinations of "simulate" parameters. + * + * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} + * class which is to be tested. + * @param sampleFile The path to the input sample. + * @param instrumentation To be used to load the sample file. + * @param expectedThrowable Expected {@link Throwable} class. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + * @see #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean) + */ + public static void assertThrows(ExtractorFactory factory, String sampleFile, + Instrumentation instrumentation, Class expectedThrowable) + throws IOException, InterruptedException { + byte[] fileData = TestUtil.getByteArray(instrumentation, sampleFile); + assertThrows(factory, fileData, expectedThrowable); + } + + /** + * Calls {@link #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)} with all + * possible combinations of "simulate" parameters. + * + * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} + * class which is to be tested. + * @param fileData Content of the input file. + * @param expectedThrowable Expected {@link Throwable} class. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + * @see #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean) + */ + public static void assertThrows(ExtractorFactory factory, byte[] fileData, + Class expectedThrowable) throws IOException, InterruptedException { + assertThrows(factory.create(), fileData, expectedThrowable, false, false, false); + assertThrows(factory.create(), fileData, expectedThrowable, true, false, false); + assertThrows(factory.create(), fileData, expectedThrowable, false, true, false); + assertThrows(factory.create(), fileData, expectedThrowable, true, true, false); + assertThrows(factory.create(), fileData, expectedThrowable, false, false, true); + assertThrows(factory.create(), fileData, expectedThrowable, true, false, true); + assertThrows(factory.create(), fileData, expectedThrowable, false, true, true); + assertThrows(factory.create(), fileData, expectedThrowable, true, true, true); + } + + /** + * Asserts {@code extractor} throws {@code expectedThrowable} while consuming {@code sampleFile}. + * + * @param extractor The {@link Extractor} to be tested. + * @param fileData Content of the input file. + * @param expectedThrowable Expected {@link Throwable} class. + * @param simulateIOErrors If true simulates IOErrors. + * @param simulateUnknownLength If true simulates unknown input length. + * @param simulatePartialReads If true simulates partial reads. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + */ + public static void assertThrows(Extractor extractor, byte[] fileData, + Class expectedThrowable, boolean simulateIOErrors, + boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException, + InterruptedException { + FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData) + .setSimulateIOErrors(simulateIOErrors) + .setSimulateUnknownLength(simulateUnknownLength) + .setSimulatePartialReads(simulatePartialReads).build(); + try { + consumeTestData(extractor, input, 0, true); + throw new AssertionError(expectedThrowable.getSimpleName() + " expected but not thrown"); + } catch (Throwable throwable) { + if (expectedThrowable.equals(throwable.getClass())) { + return; // Pass! + } + throw throwable; + } + } + + private ExtractorAsserts() {} + + private static FakeExtractorOutput consumeTestData(Extractor extractor, FakeExtractorInput input, + long timeUs, boolean retryFromStartIfLive) throws IOException, InterruptedException { + FakeExtractorOutput output = new FakeExtractorOutput(); + extractor.init(output); + consumeTestData(extractor, input, timeUs, output, retryFromStartIfLive); + return output; + } + + private static void consumeTestData(Extractor extractor, FakeExtractorInput input, long timeUs, + FakeExtractorOutput output, boolean retryFromStartIfLive) + throws IOException, InterruptedException { + extractor.seek(input.getPosition(), timeUs); + PositionHolder seekPositionHolder = new PositionHolder(); + int readResult = Extractor.RESULT_CONTINUE; + while (readResult != Extractor.RESULT_END_OF_INPUT) { + try { + // Extractor.read should not read seekPositionHolder.position. Set it to a value that's + // likely to cause test failure if a read does occur. + seekPositionHolder.position = Long.MIN_VALUE; + readResult = extractor.read(input, seekPositionHolder); + if (readResult == Extractor.RESULT_SEEK) { + long seekPosition = seekPositionHolder.position; + Assertions.checkState(0 <= seekPosition && seekPosition <= Integer.MAX_VALUE); + input.setPosition((int) seekPosition); + } + } catch (SimulatedIOException e) { + if (!retryFromStartIfLive) { + continue; + } + boolean isOnDemand = input.getLength() != C.LENGTH_UNSET + || (output.seekMap != null && output.seekMap.getDurationUs() != C.TIME_UNSET); + if (isOnDemand) { + continue; + } + input.setPosition(0); + for (int i = 0; i < output.numberOfTracks; i++) { + output.trackOutputs.valueAt(i).clear(); + } + extractor.seek(0, 0); + } + } + } + + private static boolean assetExists(Instrumentation instrumentation, String fileName) + throws IOException { + int i = fileName.lastIndexOf('/'); + String path = i >= 0 ? fileName.substring(0, i) : ""; + String file = i >= 0 ? fileName.substring(i + 1) : fileName; + return Arrays.asList(instrumentation.getContext().getResources().getAssets().list(path)) + .contains(file); + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 9a26c415f4..5819a4b711 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -21,8 +21,6 @@ import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.extractor.PositionHolder; -import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; @@ -42,23 +40,8 @@ import org.mockito.MockitoAnnotations; */ public class TestUtil { - /** - * A factory for {@link Extractor} instances. - */ - public interface ExtractorFactory { - Extractor create(); - } - - private static final String DUMP_EXTENSION = ".dump"; - private static final String UNKNOWN_LENGTH_EXTENSION = ".unklen" + DUMP_EXTENSION; - private TestUtil() {} - public static boolean sniffTestData(Extractor extractor, byte[] data) - throws IOException, InterruptedException { - return sniffTestData(extractor, newExtractorInput(data)); - } - public static boolean sniffTestData(Extractor extractor, FakeExtractorInput input) throws IOException, InterruptedException { while (true) { @@ -86,54 +69,6 @@ public class TestUtil { return Arrays.copyOf(data, position); } - public static FakeExtractorOutput consumeTestData(Extractor extractor, FakeExtractorInput input, - long timeUs) throws IOException, InterruptedException { - return consumeTestData(extractor, input, timeUs, false); - } - - public static FakeExtractorOutput consumeTestData(Extractor extractor, FakeExtractorInput input, - long timeUs, boolean retryFromStartIfLive) throws IOException, InterruptedException { - FakeExtractorOutput output = new FakeExtractorOutput(); - extractor.init(output); - consumeTestData(extractor, input, timeUs, output, retryFromStartIfLive); - return output; - } - - private static void consumeTestData(Extractor extractor, FakeExtractorInput input, long timeUs, - FakeExtractorOutput output, boolean retryFromStartIfLive) - throws IOException, InterruptedException { - extractor.seek(input.getPosition(), timeUs); - PositionHolder seekPositionHolder = new PositionHolder(); - int readResult = Extractor.RESULT_CONTINUE; - while (readResult != Extractor.RESULT_END_OF_INPUT) { - try { - // Extractor.read should not read seekPositionHolder.position. Set it to a value that's - // likely to cause test failure if a read does occur. - seekPositionHolder.position = Long.MIN_VALUE; - readResult = extractor.read(input, seekPositionHolder); - if (readResult == Extractor.RESULT_SEEK) { - long seekPosition = seekPositionHolder.position; - Assertions.checkState(0 <= seekPosition && seekPosition <= Integer.MAX_VALUE); - input.setPosition((int) seekPosition); - } - } catch (SimulatedIOException e) { - if (!retryFromStartIfLive) { - continue; - } - boolean isOnDemand = input.getLength() != C.LENGTH_UNSET - || (output.seekMap != null && output.seekMap.getDurationUs() != C.TIME_UNSET); - if (isOnDemand) { - continue; - } - input.setPosition(0); - for (int i = 0; i < output.numberOfTracks; i++) { - output.trackOutputs.valueAt(i).clear(); - } - extractor.seek(0, 0); - } - } - } - public static byte[] buildTestData(int length) { return buildTestData(length, length); } @@ -193,15 +128,6 @@ public class TestUtil { MockitoAnnotations.initMocks(instrumentationTestCase); } - public static boolean assetExists(Instrumentation instrumentation, String fileName) - throws IOException { - int i = fileName.lastIndexOf('/'); - String path = i >= 0 ? fileName.substring(0, i) : ""; - String file = i >= 0 ? fileName.substring(i + 1) : fileName; - return Arrays.asList(instrumentation.getContext().getResources().getAssets().list(path)) - .contains(file); - } - public static byte[] getByteArray(Instrumentation instrumentation, String fileName) throws IOException { return Util.toByteArray(getInputStream(instrumentation, fileName)); @@ -217,10 +143,6 @@ public class TestUtil { return new String(getByteArray(instrumentation, fileName)); } - private static FakeExtractorInput newExtractorInput(byte[] data) { - return new FakeExtractorInput.Builder().setData(data).build(); - } - /** * Extracts the timeline from a media source. */ @@ -247,180 +169,6 @@ public class TestUtil { return listener.timeline; } - /** - * Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, - * boolean)} with all possible combinations of "simulate" parameters. - * - * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} - * class which is to be tested. - * @param sampleFile The path to the input sample. - * @param instrumentation To be used to load the sample file. - * @throws IOException If reading from the input fails. - * @throws InterruptedException If interrupted while reading from the input. - * @see #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, boolean) - */ - public static void assertOutput(ExtractorFactory factory, String sampleFile, - Instrumentation instrumentation) throws IOException, InterruptedException { - byte[] fileData = getByteArray(instrumentation, sampleFile); - assertOutput(factory, sampleFile, fileData, instrumentation); - } - - /** - * Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, - * boolean)} with all possible combinations of "simulate" parameters. - * - * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} - * class which is to be tested. - * @param sampleFile The path to the input sample. - * @param fileData Content of the input file. - * @param instrumentation To be used to load the sample file. - * @throws IOException If reading from the input fails. - * @throws InterruptedException If interrupted while reading from the input. - * @see #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, boolean) - */ - public static void assertOutput(ExtractorFactory factory, String sampleFile, byte[] fileData, - Instrumentation instrumentation) throws IOException, InterruptedException { - assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, false, false); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, false, false); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, true, false); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, true, false); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, false, true); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, false, true); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, true, true); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, true, true); - } - - /** - * Asserts that {@code extractor} consumes {@code sampleFile} successfully and its output equals - * to a prerecorded output dump file with the name {@code sampleFile} + "{@value - * #DUMP_EXTENSION}". If {@code simulateUnknownLength} is true and {@code sampleFile} + "{@value - * #UNKNOWN_LENGTH_EXTENSION}" exists, it's preferred. - * - * @param extractor The {@link Extractor} to be tested. - * @param sampleFile The path to the input sample. - * @param fileData Content of the input file. - * @param instrumentation To be used to load the sample file. - * @param simulateIOErrors If true simulates IOErrors. - * @param simulateUnknownLength If true simulates unknown input length. - * @param simulatePartialReads If true simulates partial reads. - * @return The {@link FakeExtractorOutput} used in the test. - * @throws IOException If reading from the input fails. - * @throws InterruptedException If interrupted while reading from the input. - */ - public static FakeExtractorOutput assertOutput(Extractor extractor, String sampleFile, - byte[] fileData, Instrumentation instrumentation, boolean simulateIOErrors, - boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException, - InterruptedException { - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData) - .setSimulateIOErrors(simulateIOErrors) - .setSimulateUnknownLength(simulateUnknownLength) - .setSimulatePartialReads(simulatePartialReads).build(); - - Assert.assertTrue(sniffTestData(extractor, input)); - input.resetPeekPosition(); - FakeExtractorOutput extractorOutput = consumeTestData(extractor, input, 0, true); - - if (simulateUnknownLength - && assetExists(instrumentation, sampleFile + UNKNOWN_LENGTH_EXTENSION)) { - extractorOutput.assertOutput(instrumentation, sampleFile + UNKNOWN_LENGTH_EXTENSION); - } else { - extractorOutput.assertOutput(instrumentation, sampleFile + ".0" + DUMP_EXTENSION); - } - - SeekMap seekMap = extractorOutput.seekMap; - if (seekMap.isSeekable()) { - long durationUs = seekMap.getDurationUs(); - for (int j = 0; j < 4; j++) { - long timeUs = (durationUs * j) / 3; - long position = seekMap.getPosition(timeUs); - 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(instrumentation, sampleFile + '.' + j + DUMP_EXTENSION); - } - } - - return extractorOutput; - } - - /** - * Calls {@link #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)} with all - * possible combinations of "simulate" parameters. - * - * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} - * class which is to be tested. - * @param sampleFile The path to the input sample. - * @param instrumentation To be used to load the sample file. - * @param expectedThrowable Expected {@link Throwable} class. - * @throws IOException If reading from the input fails. - * @throws InterruptedException If interrupted while reading from the input. - * @see #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean) - */ - public static void assertThrows(ExtractorFactory factory, String sampleFile, - Instrumentation instrumentation, Class expectedThrowable) - throws IOException, InterruptedException { - byte[] fileData = getByteArray(instrumentation, sampleFile); - assertThrows(factory, fileData, expectedThrowable); - } - - /** - * Calls {@link #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)} with all - * possible combinations of "simulate" parameters. - * - * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} - * class which is to be tested. - * @param fileData Content of the input file. - * @param expectedThrowable Expected {@link Throwable} class. - * @throws IOException If reading from the input fails. - * @throws InterruptedException If interrupted while reading from the input. - * @see #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean) - */ - public static void assertThrows(ExtractorFactory factory, byte[] fileData, - Class expectedThrowable) throws IOException, InterruptedException { - assertThrows(factory.create(), fileData, expectedThrowable, false, false, false); - assertThrows(factory.create(), fileData, expectedThrowable, true, false, false); - assertThrows(factory.create(), fileData, expectedThrowable, false, true, false); - assertThrows(factory.create(), fileData, expectedThrowable, true, true, false); - assertThrows(factory.create(), fileData, expectedThrowable, false, false, true); - assertThrows(factory.create(), fileData, expectedThrowable, true, false, true); - assertThrows(factory.create(), fileData, expectedThrowable, false, true, true); - assertThrows(factory.create(), fileData, expectedThrowable, true, true, true); - } - - /** - * Asserts {@code extractor} throws {@code expectedThrowable} while consuming {@code sampleFile}. - * - * @param extractor The {@link Extractor} to be tested. - * @param fileData Content of the input file. - * @param expectedThrowable Expected {@link Throwable} class. - * @param simulateIOErrors If true simulates IOErrors. - * @param simulateUnknownLength If true simulates unknown input length. - * @param simulatePartialReads If true simulates partial reads. - * @throws IOException If reading from the input fails. - * @throws InterruptedException If interrupted while reading from the input. - */ - public static void assertThrows(Extractor extractor, byte[] fileData, - Class expectedThrowable, boolean simulateIOErrors, - boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException, - InterruptedException { - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData) - .setSimulateIOErrors(simulateIOErrors) - .setSimulateUnknownLength(simulateUnknownLength) - .setSimulatePartialReads(simulatePartialReads).build(); - try { - consumeTestData(extractor, input, 0, true); - throw new AssertionError(expectedThrowable.getSimpleName() + " expected but not thrown"); - } catch (Throwable throwable) { - if (expectedThrowable.equals(throwable.getClass())) { - return; // Pass! - } - throw throwable; - } - } - /** * Asserts that data read from a {@link DataSource} matches {@code expected}. * From 37faead26ed36e23b85af13bfe0873e68e09e3c5 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 4 Jul 2017 01:53:38 -0700 Subject: [PATCH 218/220] Rename some assert methods in CacheAsserts to better reflect what they do ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160873280 --- .../exoplayer2/testutil/CacheAsserts.java | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java index 3494998e04..c527f14c5a 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheUtil; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.ArrayList; import junit.framework.Assert; /** @@ -38,31 +39,38 @@ public final class CacheAsserts { /** Asserts that the cache content is equal to the data in the {@code fakeDataSet}. */ public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException { + ArrayList allData = fakeDataSet.getAllData(); + String[] uriStrings = new String[allData.size()]; + for (int i = 0; i < allData.size(); i++) { + uriStrings[i] = allData.get(i).uri; + } + assertCachedData(cache, fakeDataSet, uriStrings); + } + + /** + * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. + */ + public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, String... uriStrings) + throws IOException { int totalLength = 0; - for (FakeData fakeData : fakeDataSet.getAllData()) { - byte[] data = fakeData.getData(); - assertCachedData(cache, fakeData.uri, data); + for (String uriString : uriStrings) { + byte[] data = fakeDataSet.getData(uriString).getData(); + assertDataCached(cache, uriString, data); totalLength += data.length; } assertEquals(totalLength, cache.getCacheSpace()); } - /** - * Asserts that the cache content for the given {@code uriStrings} are equal to the data in the - * {@code fakeDataSet}. - */ - public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, String... uriStrings) + /** Asserts that the cache contains the given subset of data in the {@code fakeDataSet}. */ + public static void assertDataCached(Cache cache, FakeDataSet fakeDataSet, String... uriStrings) throws IOException { for (String uriString : uriStrings) { - assertCachedData(cache, uriString, fakeDataSet.getData(uriString).getData()); + assertDataCached(cache, uriString, fakeDataSet.getData(uriString).getData()); } } - /** - * Asserts that the cache content for the given {@code uriString} is equal to the {@code - * expected}. - */ - public static void assertCachedData(Cache cache, String uriString, byte[] expected) + /** Asserts that the cache contains the given data for {@code uriString}. */ + public static void assertDataCached(Cache cache, String uriString, byte[] expected) throws IOException { CacheDataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); @@ -85,18 +93,14 @@ public final class CacheAsserts { } /** Asserts that there is no cache content for the given {@code uriStrings}. */ - public static void assertNoCachedData(Cache cache, String... uriStrings) { + public static void assertDataNotCached(Cache cache, String... uriStrings) { for (String uriString : uriStrings) { Assert.assertNull("There is cached data for '" + uriString + "',", cache.getCachedSpans(CacheUtil.generateKey(Uri.parse(uriString)))); } } - /** - * Asserts that the cache is empty. - * - * @param cache - */ + /** Asserts that the cache is empty. */ public static void assertCacheEmpty(Cache cache) { assertEquals(0, cache.getCacheSpace()); } From dda3616f5ad2b83016bfe1f8a7fe427b29670a41 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 4 Jul 2017 03:32:23 -0700 Subject: [PATCH 219/220] Fix reporting of width/height 1. maybeRenotifyVideoSizeChanged should report reported* variables 2. Add check into maybeNotifyVideoSizeChanged to suppress reporting in the case that the width and height are still unknown. Issue: #3007 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160879625 --- .../exoplayer2/video/MediaCodecVideoRenderer.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 8878cf2e73..07c45dcd25 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 @@ -700,9 +700,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } private void maybeNotifyVideoSizeChanged() { - if (reportedWidth != currentWidth || reportedHeight != currentHeight + if ((currentWidth != Format.NO_VALUE || currentHeight != Format.NO_VALUE) + && (reportedWidth != currentWidth || reportedHeight != currentHeight || reportedUnappliedRotationDegrees != currentUnappliedRotationDegrees - || reportedPixelWidthHeightRatio != currentPixelWidthHeightRatio) { + || reportedPixelWidthHeightRatio != currentPixelWidthHeightRatio)) { eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees, currentPixelWidthHeightRatio); reportedWidth = currentWidth; @@ -714,8 +715,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private void maybeRenotifyVideoSizeChanged() { if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) { - eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees, - currentPixelWidthHeightRatio); + eventDispatcher.videoSizeChanged(reportedWidth, reportedHeight, + reportedUnappliedRotationDegrees, reportedPixelWidthHeightRatio); } } From a04663372fe6f3538fd27c322054769996bf84e4 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 4 Jul 2017 09:38:57 -0700 Subject: [PATCH 220/220] Detect playlist stuck and playlist reset conditions in HLS This CL aims that the player fails upon: - Playlist that don't change in a suspiciously long time, which might mean there are server side issues. - Playlist with a media sequence lower that its last snapshot and no overlapping segments. This two error conditions are propagated through the renderer, but not through MediaSource#maybeThrowSourceInfoRefreshError. Issue:#2872 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160899995 --- .../hls/playlist/HlsPlaylistTracker.java | 82 ++++++++++++++++--- 1 file changed, 72 insertions(+), 10 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index 62b77a0575..567dbd4af6 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -40,6 +40,38 @@ import java.util.List; */ public final class HlsPlaylistTracker implements Loader.Callback> { + /** + * Thrown when a playlist is considered to be stuck due to a server side error. + */ + public static final class PlaylistStuckException extends IOException { + + /** + * The url of the stuck playlist. + */ + public final String url; + + private PlaylistStuckException(String url) { + this.url = url; + } + + } + + /** + * Thrown when the media sequence of a new snapshot indicates the server has reset. + */ + public static final class PlaylistResetException extends IOException { + + /** + * The url of the reset playlist. + */ + public final String url; + + private PlaylistResetException(String url) { + this.url = url; + } + + } + /** * Listener for primary playlist changes. */ @@ -75,6 +107,11 @@ public final class HlsPlaylistTracker implements Loader.Callback C.usToMs(playlistSnapshot.targetDurationUs) + * PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) { + // The playlist seems to be stuck, we blacklist it. + playlistError = new PlaylistStuckException(playlistUrl.url); + blacklistPlaylist(); + } else if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size() + < playlistSnapshot.mediaSequence) { + // The media sequence has jumped backwards. The server has likely reset. + playlistError = new PlaylistResetException(playlistUrl.url); + } refreshDelayUs = playlistSnapshot.targetDurationUs / 2; } if (refreshDelayUs != C.TIME_UNSET) { @@ -554,6 +610,12 @@ public final class HlsPlaylistTracker implements Loader.Callback