From f764fe70b0306d7650cf83345f06ce3dd753470e Mon Sep 17 00:00:00 2001 From: Bei Yi Date: Thu, 17 Aug 2017 11:01:42 -0700 Subject: [PATCH 0001/1327] Support crop mode for AspectRatioFrameLayout --- .../exoplayer2/ui/AspectRatioFrameLayout.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index b0df16b484..2f04b8800d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -32,7 +32,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { * Resize modes for {@link AspectRatioFrameLayout}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL}) + @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_CROP}) public @interface ResizeMode {} /** @@ -51,6 +51,10 @@ public final class AspectRatioFrameLayout extends FrameLayout { * The specified aspect ratio is ignored. */ public static final int RESIZE_MODE_FILL = 3; + /** + * The height or width is increased or decreased to crop and to obtain the desired aspect ratio. + */ + public static final int RESIZE_MODE_CROP = 4; /** * The {@link FrameLayout} will not resize itself if the fractional difference between its natural @@ -96,6 +100,15 @@ public final class AspectRatioFrameLayout extends FrameLayout { } } + /** + * Gets the resize mode. + * + * @return The resize mode. + */ + public int getResizeMode() { + return this.resizeMode; + } + /** * Sets the resize mode. * @@ -132,6 +145,13 @@ public final class AspectRatioFrameLayout extends FrameLayout { case RESIZE_MODE_FIXED_HEIGHT: width = (int) (height * videoAspectRatio); break; + case RESIZE_MODE_CROP: + if (videoAspectRatio > viewAspectRatio) { + width = (int) (height * videoAspectRatio); + } else { + height = (int) (width / videoAspectRatio); + } + break; default: if (aspectDeformation > 0) { height = (int) (width / videoAspectRatio); From 50c485652cb0b52f593b8a4035dcd50e050747e1 Mon Sep 17 00:00:00 2001 From: Bei Yi Date: Thu, 24 Aug 2017 14:31:33 -0700 Subject: [PATCH 0002/1327] Support aspect ratio fill mode for AspectRatioFrameLayout --- .../android/exoplayer2/ui/AspectRatioFrameLayout.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index 2f04b8800d..9b93b3a867 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -32,7 +32,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { * Resize modes for {@link AspectRatioFrameLayout}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_CROP}) + @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_ASPECT_FILL}) public @interface ResizeMode {} /** @@ -52,9 +52,9 @@ public final class AspectRatioFrameLayout extends FrameLayout { */ public static final int RESIZE_MODE_FILL = 3; /** - * The height or width is increased or decreased to crop and to obtain the desired aspect ratio. + * Either height or width is increased to obtain the desired aspect ratio. */ - public static final int RESIZE_MODE_CROP = 4; + public static final int RESIZE_MODE_ASPECT_FILL = 4; /** * The {@link FrameLayout} will not resize itself if the fractional difference between its natural @@ -145,7 +145,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { case RESIZE_MODE_FIXED_HEIGHT: width = (int) (height * videoAspectRatio); break; - case RESIZE_MODE_CROP: + case RESIZE_MODE_ASPECT_FILL: if (videoAspectRatio > viewAspectRatio) { width = (int) (height * videoAspectRatio); } else { From b0848216786d6ebfd35c898286ebdd22e4aa5234 Mon Sep 17 00:00:00 2001 From: Bei Yi Date: Thu, 24 Aug 2017 16:13:44 -0700 Subject: [PATCH 0003/1327] Support zoom mode for AspectRatioFrameLayout --- .../android/exoplayer2/ui/AspectRatioFrameLayout.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index 9b93b3a867..3367a46374 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -32,7 +32,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { * Resize modes for {@link AspectRatioFrameLayout}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_ASPECT_FILL}) + @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_ZOOM}) public @interface ResizeMode {} /** @@ -54,7 +54,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { /** * Either height or width is increased to obtain the desired aspect ratio. */ - public static final int RESIZE_MODE_ASPECT_FILL = 4; + public static final int RESIZE_MODE_ZOOM = 4; /** * The {@link FrameLayout} will not resize itself if the fractional difference between its natural @@ -145,7 +145,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { case RESIZE_MODE_FIXED_HEIGHT: width = (int) (height * videoAspectRatio); break; - case RESIZE_MODE_ASPECT_FILL: + case RESIZE_MODE_ZOOM: if (videoAspectRatio > viewAspectRatio) { width = (int) (height * videoAspectRatio); } else { From 10f1bd73961b972ec451076ed539ab7f189b1e62 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 01:08:08 -0700 Subject: [PATCH 0004/1327] Add shuffle mode parameter to Timeline interface methods. This parameter is used by methods such as getNextWindowIndex and getPreviousWindowIndex to determine the playback order. Additionally, there are method to query the first and last window index given the shuffle mode. None of the timeline implementations nor the ExoPlayer implementation supports shuffling so far. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166170229 --- .../mediasession/TimelineQueueNavigator.java | 4 +- .../exoplayer2/ExoPlayerImplInternal.java | 7 +- .../exoplayer2/MediaPeriodInfoSequence.java | 4 +- .../google/android/exoplayer2/Timeline.java | 63 +++- .../exoplayer2/offline/DownloadException.java | 30 ++ .../exoplayer2/offline/Downloader.java | 94 +++++ .../offline/DownloaderConstructorHelper.java | 107 ++++++ .../offline/ProgressiveDownloader.java | 100 ++++++ .../exoplayer2/offline/SegmentDownloader.java | 331 ++++++++++++++++++ .../source/AbstractConcatenatedTimeline.java | 12 +- .../source/ConcatenatingMediaSource.java | 10 +- .../exoplayer2/source/ForwardingTimeline.java | 20 +- .../exoplayer2/source/LoopingMediaSource.java | 12 +- .../hls/offline/HlsDownloadTestData.java | 80 +++++ .../source/hls/offline/HlsDownloaderTest.java | 210 +++++++++++ .../source/hls/offline/HlsDownloadAction.java | 83 +++++ .../source/hls/offline/HlsDownloader.java | 139 ++++++++ .../offline/SsDownloadAction.java | 86 +++++ .../smoothstreaming/offline/SsDownloader.java | 111 ++++++ .../exoplayer2/ui/PlaybackControlView.java | 11 +- .../playbacktests/gts/DashDownloadTest.java | 192 ++++++++++ .../exoplayer2/testutil/TimelineAsserts.java | 27 +- 22 files changed, 1679 insertions(+), 54 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java create mode 100644 library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java create mode 100644 library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java create mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java create mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java create mode 100644 playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 435d994dcc..bd3f3f2820 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -127,7 +127,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu return; } int previousWindowIndex = timeline.getPreviousWindowIndex(player.getCurrentWindowIndex(), - player.getRepeatMode()); + player.getRepeatMode(), false); if (player.getCurrentPosition() > MAX_POSITION_FOR_SEEK_TO_PREVIOUS || previousWindowIndex == C.INDEX_UNSET) { player.seekTo(0); @@ -155,7 +155,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu return; } int nextWindowIndex = timeline.getNextWindowIndex(player.getCurrentWindowIndex(), - player.getRepeatMode()); + player.getRepeatMode(), false); if (nextWindowIndex != C.INDEX_UNSET) { player.seekTo(nextWindowIndex, C.TIME_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 67586cc07a..7035ed637e 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,7 +488,7 @@ import java.io.IOException; } while (true) { int nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.info.id.periodIndex, - period, window, repeatMode); + period, window, repeatMode, false); while (lastValidPeriodHolder.next != null && !lastValidPeriodHolder.info.isLastInTimelinePeriod) { lastValidPeriodHolder = lastValidPeriodHolder.next; @@ -1122,7 +1122,7 @@ import java.io.IOException; while (periodHolder.next != null) { MediaPeriodHolder previousPeriodHolder = periodHolder; periodHolder = periodHolder.next; - periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode); + periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode, false); 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. @@ -1204,7 +1204,8 @@ 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, repeatMode); + oldPeriodIndex = oldTimeline.getNextPeriodIndex(oldPeriodIndex, period, window, repeatMode, + false); if (oldPeriodIndex == C.INDEX_UNSET) { // We've reached the end of the old timeline. break; 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 index 9e8c2645c1..d7821ed705 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java @@ -162,7 +162,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; // timeline is updated, to avoid repeatedly checking the same timeline. if (currentMediaPeriodInfo.isLastInTimelinePeriod) { int nextPeriodIndex = timeline.getNextPeriodIndex(currentMediaPeriodInfo.id.periodIndex, - period, window, repeatMode); + period, window, repeatMode, false); if (nextPeriodIndex == C.INDEX_UNSET) { // We can't create a next period yet. return null; @@ -353,7 +353,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; 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) + && timeline.isLastPeriod(id.periodIndex, period, window, repeatMode, false) && 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 7d4c1995eb..8a1d7964ee 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 @@ -553,20 +553,24 @@ public abstract class Timeline { /** * Returns the index of the window after the window at index {@code windowIndex} depending on the - * {@code repeatMode}. + * {@code repeatMode} and whether shuffling is enabled. * * @param windowIndex Index of a window in the timeline. * @param repeatMode A repeat mode. + * @param shuffleModeEnabled Whether shuffling is enabled. * @return The index of the next window, or {@link C#INDEX_UNSET} if this is the last window. */ - public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { switch (repeatMode) { case Player.REPEAT_MODE_OFF: - return windowIndex == getWindowCount() - 1 ? C.INDEX_UNSET : windowIndex + 1; + return windowIndex == getLastWindowIndex(shuffleModeEnabled) ? C.INDEX_UNSET + : windowIndex + 1; case Player.REPEAT_MODE_ONE: return windowIndex; case Player.REPEAT_MODE_ALL: - return windowIndex == getWindowCount() - 1 ? 0 : windowIndex + 1; + return windowIndex == getLastWindowIndex(shuffleModeEnabled) + ? getFirstWindowIndex(shuffleModeEnabled) : windowIndex + 1; default: throw new IllegalStateException(); } @@ -574,25 +578,51 @@ public abstract class Timeline { /** * Returns the index of the window before the window at index {@code windowIndex} depending on the - * {@code repeatMode}. + * {@code repeatMode} and whether shuffling is enabled. * * @param windowIndex Index of a window in the timeline. * @param repeatMode A repeat mode. + * @param shuffleModeEnabled Whether shuffling is enabled. * @return The index of the previous window, or {@link C#INDEX_UNSET} if this is the first window. */ - public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { switch (repeatMode) { case Player.REPEAT_MODE_OFF: - return windowIndex == 0 ? C.INDEX_UNSET : windowIndex - 1; + return windowIndex == getFirstWindowIndex(shuffleModeEnabled) ? C.INDEX_UNSET + : windowIndex - 1; case Player.REPEAT_MODE_ONE: return windowIndex; case Player.REPEAT_MODE_ALL: - return windowIndex == 0 ? getWindowCount() - 1 : windowIndex - 1; + return windowIndex == getFirstWindowIndex(shuffleModeEnabled) + ? getLastWindowIndex(shuffleModeEnabled) : windowIndex - 1; default: throw new IllegalStateException(); } } + /** + * Returns the index of the last window in the playback order depending on whether shuffling is + * enabled. + * + * @param shuffleModeEnabled Whether shuffling is enabled. + * @return The index of the last window in the playback order. + */ + public int getLastWindowIndex(boolean shuffleModeEnabled) { + return getWindowCount() - 1; + } + + /** + * Returns the index of the first window in the playback order depending on whether shuffling is + * enabled. + * + * @param shuffleModeEnabled Whether shuffling is enabled. + * @return The index of the first window in the playback order. + */ + public int getFirstWindowIndex(boolean shuffleModeEnabled) { + return 0; + } + /** * Populates a {@link Window} with data for the window at the specified index. Does not populate * {@link Window#id}. @@ -614,7 +644,7 @@ public abstract class Timeline { * null. The caller should pass false for efficiency reasons unless the field is required. * @return The populated {@link Window}, for convenience. */ - public Window getWindow(int windowIndex, Window window, boolean setIds) { + public final Window getWindow(int windowIndex, Window window, boolean setIds) { return getWindow(windowIndex, window, setIds, 0); } @@ -639,19 +669,20 @@ public abstract class Timeline { /** * Returns the index of the period after the period at index {@code periodIndex} depending on the - * {@code repeatMode}. + * {@code repeatMode} and whether shuffling is enabled. * * @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. + * @param shuffleModeEnabled Whether shuffling is enabled. * @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, - @Player.RepeatMode int repeatMode) { + @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { int windowIndex = getPeriod(periodIndex, period).windowIndex; if (getWindow(windowIndex, window).lastPeriodIndex == periodIndex) { - int nextWindowIndex = getNextWindowIndex(windowIndex, repeatMode); + int nextWindowIndex = getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); if (nextWindowIndex == C.INDEX_UNSET) { return C.INDEX_UNSET; } @@ -662,17 +693,19 @@ public abstract class Timeline { /** * Returns whether the given period is the last period of the timeline depending on the - * {@code repeatMode}. + * {@code repeatMode} and whether shuffling is enabled. * * @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. + * @param shuffleModeEnabled Whether shuffling is enabled. * @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, - @Player.RepeatMode int repeatMode) { - return getNextPeriodIndex(periodIndex, period, window, repeatMode) == C.INDEX_UNSET; + @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + return getNextPeriodIndex(periodIndex, period, window, repeatMode, shuffleModeEnabled) + == C.INDEX_UNSET; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java new file mode 100644 index 0000000000..239195892c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java @@ -0,0 +1,30 @@ +/* + * 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.offline; + +import com.google.android.exoplayer2.util.ClosedSource; +import java.io.IOException; + +/** Thrown on an error during downloading. */ +@ClosedSource(reason = "Not ready yet") +public final class DownloadException extends IOException { + + /** @param message The message for the exception. */ + public DownloadException(String message) { + super(message); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java new file mode 100644 index 0000000000..a130bb4052 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java @@ -0,0 +1,94 @@ +/* + * 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.offline; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.ClosedSource; +import java.io.IOException; + +/** + * An interface for stream downloaders. + */ +@ClosedSource(reason = "Not ready yet") +public interface Downloader { + + /** + * Listener notified when download progresses. + *

+ * No guarantees are made about the thread or threads on which the listener is called, but it is + * guaranteed that listener methods will be called in a serial fashion (i.e. one at a time) and in + * the same order as events occurred. + */ + interface ProgressListener { + /** + * Called during the download. Calling intervals depend on the {@link Downloader} + * implementation. + * + * @param downloader The reporting instance. + * @param downloadPercentage The download percentage. This value can be an estimation. + * @param downloadedBytes Total number of downloaded bytes. + * @see #download(ProgressListener) + */ + void onDownloadProgress(Downloader downloader, float downloadPercentage, long downloadedBytes); + } + + /** + * Initializes the downloader. + * + * @throws DownloadException Thrown if the media cannot be downloaded. + * @throws InterruptedException If the thread has been interrupted. + * @throws IOException Thrown when there is an io error while reading from cache. + * @see #getDownloadedBytes() + * @see #getDownloadPercentage() + */ + void init() throws InterruptedException, IOException; + + /** + * Downloads the media. + * + * @param listener If not null, called during download. + * @throws DownloadException Thrown if the media cannot be downloaded. + * @throws InterruptedException If the thread has been interrupted. + * @throws IOException Thrown when there is an io error while downloading. + */ + void download(@Nullable ProgressListener listener) + throws InterruptedException, IOException; + + /** + * Removes all of the downloaded data of the media. + * + * @throws InterruptedException Thrown if the thread was interrupted. + */ + void remove() throws InterruptedException; + + /** + * Returns the total number of downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been + * calculated yet. + * + * @see #init() + */ + long getDownloadedBytes(); + + /** + * Returns the download percentage, or {@link Float#NaN} if it can't be calculated yet. This + * value can be an estimation. + * + * @see #init() + */ + float getDownloadPercentage(); + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java new file mode 100644 index 0000000000..5f9a4d973a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java @@ -0,0 +1,107 @@ +/* + * 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.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.DataSource.Factory; +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.Assertions; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.PriorityTaskManager; + +/** A helper class that holds necessary parameters for {@link Downloader} construction. */ +@ClosedSource(reason = "Not ready yet") +public final class DownloaderConstructorHelper { + + private final Cache cache; + private final Factory upstreamDataSourceFactory; + private final Factory cacheReadDataSourceFactory; + private final DataSink.Factory cacheWriteDataSinkFactory; + private final PriorityTaskManager priorityTaskManager; + + /** + * @param cache Cache instance to be used to store downloaded data. + * @param upstreamDataSourceFactory A {@link Factory} for downloading data. + */ + public DownloaderConstructorHelper(Cache cache, Factory upstreamDataSourceFactory) { + this(cache, upstreamDataSourceFactory, null, null, null); + } + + /** + * @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 DownloaderConstructorHelper(Cache cache, Factory upstreamDataSourceFactory, + @Nullable Factory cacheReadDataSourceFactory, + @Nullable DataSink.Factory cacheWriteDataSinkFactory, + @Nullable PriorityTaskManager priorityTaskManager) { + Assertions.checkNotNull(upstreamDataSourceFactory); + this.cache = cache; + this.upstreamDataSourceFactory = upstreamDataSourceFactory; + this.cacheReadDataSourceFactory = cacheReadDataSourceFactory; + this.cacheWriteDataSinkFactory = cacheWriteDataSinkFactory; + this.priorityTaskManager = priorityTaskManager; + } + + /** Returns the {@link Cache} instance. */ + public Cache getCache() { + return cache; + } + + /** Returns a {@link PriorityTaskManager} instance.*/ + public PriorityTaskManager getPriorityTaskManager() { + // Return a dummy PriorityTaskManager if none is provided. Create a new PriorityTaskManager + // each time so clients don't affect each other over the dummy PriorityTaskManager instance. + return priorityTaskManager != null ? priorityTaskManager : new PriorityTaskManager(); + } + + /** + * Returns a new {@link CacheDataSource} instance. If {@code offline} is true, it can only read + * data from the cache. + */ + public CacheDataSource buildCacheDataSource(boolean offline) { + DataSource cacheReadDataSource = cacheReadDataSourceFactory != null + ? cacheReadDataSourceFactory.createDataSource() : new FileDataSource(); + if (offline) { + return new CacheDataSource(cache, DummyDataSource.INSTANCE, + cacheReadDataSource, null, CacheDataSource.FLAG_BLOCK_ON_CACHE, null); + } else { + DataSink cacheWriteDataSink = cacheWriteDataSinkFactory != null + ? cacheWriteDataSinkFactory.createDataSink() + : new CacheDataSink(cache, CacheDataSource.DEFAULT_MAX_CACHE_FILE_SIZE); + DataSource upstream = upstreamDataSourceFactory.createDataSource(); + upstream = priorityTaskManager == null ? upstream + : new PriorityDataSource(upstream, priorityTaskManager, C.PRIORITY_DOWNLOAD); + return new CacheDataSource(cache, upstream, cacheReadDataSource, + cacheWriteDataSink, CacheDataSource.FLAG_BLOCK_ON_CACHE, null); + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java new file mode 100644 index 0000000000..c6bb3bc432 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java @@ -0,0 +1,100 @@ +/* + * 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.offline; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataSpec; +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 com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.PriorityTaskManager; +import java.io.IOException; + +/** + * A downloader for progressive media streams. + */ +@ClosedSource(reason = "Not ready yet") +public final class ProgressiveDownloader implements Downloader { + + private static final int BUFFER_SIZE_BYTES = 128 * 1024; + + private final DataSpec dataSpec; + private final Cache cache; + private final CacheDataSource dataSource; + private final PriorityTaskManager priorityTaskManager; + private final CacheUtil.CachingCounters cachingCounters; + + /** + * @param uri Uri of the data to be downloaded. + * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache + * indexing. May be null. + * @param constructorHelper a {@link DownloaderConstructorHelper} instance. + */ + public ProgressiveDownloader( + String uri, String customCacheKey, DownloaderConstructorHelper constructorHelper) { + this.dataSpec = new DataSpec(Uri.parse(uri), 0, C.LENGTH_UNSET, customCacheKey, 0); + this.cache = constructorHelper.getCache(); + this.dataSource = constructorHelper.buildCacheDataSource(false); + this.priorityTaskManager = constructorHelper.getPriorityTaskManager(); + cachingCounters = new CachingCounters(); + } + + @Override + public void init() { + CacheUtil.getCached(dataSpec, cache, cachingCounters); + } + + @Override + public void download(@Nullable ProgressListener listener) throws InterruptedException, + IOException { + priorityTaskManager.add(C.PRIORITY_DOWNLOAD); + try { + byte[] buffer = new byte[BUFFER_SIZE_BYTES]; + CacheUtil.cache(dataSpec, cache, dataSource, buffer, priorityTaskManager, C.PRIORITY_DOWNLOAD, + cachingCounters, true); + // TODO: Work out how to call onDownloadProgress periodically during the download, or else + // get rid of ProgressListener and move to a model where the manager periodically polls + // Downloaders. + if (listener != null) { + listener.onDownloadProgress(this, 100, cachingCounters.contentLength); + } + } finally { + priorityTaskManager.remove(C.PRIORITY_DOWNLOAD); + } + } + + @Override + public void remove() { + CacheUtil.remove(cache, CacheUtil.getKey(dataSpec)); + } + + @Override + public long getDownloadedBytes() { + return cachingCounters.totalCachedBytes(); + } + + @Override + public float getDownloadPercentage() { + long contentLength = cachingCounters.contentLength; + return contentLength == C.LENGTH_UNSET ? Float.NaN + : ((cachingCounters.totalCachedBytes() * 100f) / contentLength); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java new file mode 100644 index 0000000000..93e7c57470 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -0,0 +1,331 @@ +/* + * 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.offline; + +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.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +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 com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.PriorityTaskManager; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +/** + * Base class for multi segment 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 SegmentDownloader implements Downloader { + + /** Smallest unit of content to be downloaded. */ + protected static class Segment implements Comparable { + /** The start time of the segment in microseconds. */ + public final long startTimeUs; + + /** The {@link DataSpec} of the segment. */ + public final DataSpec dataSpec; + + /** Constructs a Segment. */ + public Segment(long startTimeUs, DataSpec dataSpec) { + this.startTimeUs = startTimeUs; + this.dataSpec = dataSpec; + } + + @Override + public int compareTo(@NonNull Segment other) { + long startOffsetDiff = startTimeUs - other.startTimeUs; + return startOffsetDiff == 0 ? 0 : ((startOffsetDiff < 0) ? -1 : 1); + } + } + + private static final int BUFFER_SIZE_BYTES = 128 * 1024; + + private final Uri manifestUri; + private final PriorityTaskManager priorityTaskManager; + private final Cache cache; + private final CacheDataSource dataSource; + private final CacheDataSource offlineDataSource; + + private M manifest; + private K[] keys; + private volatile int totalSegments; + private volatile int downloadedSegments; + private volatile long downloadedBytes; + + /** + * @param manifestUri The {@link Uri} of the manifest to be downloaded. + * @param constructorHelper a {@link DownloaderConstructorHelper} instance. + */ + public SegmentDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) { + this.manifestUri = manifestUri; + this.cache = constructorHelper.getCache(); + this.dataSource = constructorHelper.buildCacheDataSource(false); + this.offlineDataSource = constructorHelper.buildCacheDataSource(true); + this.priorityTaskManager = constructorHelper.getPriorityTaskManager(); + resetCounters(); + } + + /** + * Returns the manifest. Downloads and parses it if necessary. + * + * @return The manifest. + * @throws IOException If an error occurs reading data. + */ + public final M getManifest() throws IOException { + return getManifestIfNeeded(false); + } + + /** + * Selects multiple representations pointed to by the keys for downloading, checking status. Any + * previous selection is cleared. If keys are null or empty, all representations are downloaded. + */ + public final void selectRepresentations(K[] keys) { + this.keys = keys != null ? keys.clone() : null; + resetCounters(); + } + + /** + * Initializes the total segments, downloaded segments and downloaded bytes counters for the + * selected representations. + * + * @throws IOException Thrown when there is an io error while reading from cache. + * @throws DownloadException Thrown if the media cannot be downloaded. + * @throws InterruptedException If the thread has been interrupted. + * @see #getTotalSegments() + * @see #getDownloadedSegments() + * @see #getDownloadedBytes() + */ + @Override + public final void init() throws InterruptedException, IOException { + try { + getManifestIfNeeded(true); + } catch (IOException e) { + // Either the manifest file isn't available offline or not parsable. + return; + } + try { + initStatus(true); + } catch (IOException | InterruptedException e) { + resetCounters(); + throw e; + } + } + + /** + * Downloads the content for the selected representations in sync or resumes a previously stopped + * download. + * + * @param listener If not null, called during download. + * @throws IOException Thrown when there is an io error while downloading. + * @throws DownloadException Thrown if the media cannot be downloaded. + * @throws InterruptedException If the thread has been interrupted. + */ + @Override + public final synchronized void download(@Nullable ProgressListener listener) + throws IOException, InterruptedException { + priorityTaskManager.add(C.PRIORITY_DOWNLOAD); + try { + getManifestIfNeeded(false); + List segments = initStatus(false); + notifyListener(listener); // Initial notification. + Collections.sort(segments); + byte[] buffer = new byte[BUFFER_SIZE_BYTES]; + CachingCounters cachingCounters = new CachingCounters(); + for (int i = 0; i < segments.size(); i++) { + CacheUtil.cache(segments.get(i).dataSpec, cache, dataSource, buffer, + priorityTaskManager, C.PRIORITY_DOWNLOAD, cachingCounters, true); + downloadedBytes += cachingCounters.newlyCachedBytes; + downloadedSegments++; + notifyListener(listener); + } + } finally { + priorityTaskManager.remove(C.PRIORITY_DOWNLOAD); + } + } + + /** + * 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 #init() + */ + 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 #init() + */ + 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 #init() + */ + @Override + public final long getDownloadedBytes() { + return downloadedBytes; + } + + @Override + public float getDownloadPercentage() { + // Take local snapshot of the volatile fields + int totalSegments = this.totalSegments; + int downloadedSegments = this.downloadedSegments; + if (totalSegments == C.LENGTH_UNSET || downloadedSegments == C.LENGTH_UNSET) { + return Float.NaN; + } + return totalSegments == 0 ? 100f : (downloadedSegments * 100f) / totalSegments; + } + + @Override + public final void remove() throws InterruptedException { + try { + getManifestIfNeeded(true); + } catch (IOException e) { + // Either the manifest file isn't available offline, or it's not parsable. Continue anyway to + // reset the counters and attempt to remove the manifest file. + } + resetCounters(); + if (manifest != null) { + List segments = null; + try { + segments = getAllSegments(offlineDataSource, manifest, true); + } catch (IOException e) { + // Ignore exceptions. We do our best with what's available offline. + } + if (segments != null) { + for (int i = 0; i < segments.size(); i++) { + remove(segments.get(i).dataSpec.uri); + } + } + manifest = null; + } + remove(manifestUri); + } + + /** + * Loads and parses the manifest. + * + * @param dataSource The {@link DataSource} through which to load. + * @param uri The manifest uri. + * @return The manifest. + * @throws IOException If an error occurs reading data. + */ + protected abstract M getManifest(DataSource dataSource, Uri uri) throws IOException; + + /** + * Returns a list of {@link Segment}s for given keys. + * + * @param dataSource The {@link DataSource} through which to load any required data. + * @param manifest The manifest containing the segments. + * @param keys The selected representation keys. + * @param allowIncompleteIndex Whether to continue in the case that a load error prevents all + * segments from being listed. If true then a partial segment list will be returned. If false + * an {@link IOException} will be thrown. + * @throws InterruptedException Thrown if the thread was interrupted. + * @throws IOException Thrown if {@code allowPartialIndex} is false and a load error occurs, or if + * the media is not in a form that allows for its segments to be listed. + * @return A list of {@link Segment}s for given keys. + */ + protected abstract List getSegments(DataSource dataSource, M manifest, K[] keys, + boolean allowIncompleteIndex) throws InterruptedException, IOException; + + /** + * Returns a list of all segments. + * + * @see #getSegments(DataSource, M, Object[], boolean)}. + */ + protected abstract List getAllSegments(DataSource dataSource, M manifest, + boolean allowPartialIndex) throws InterruptedException, IOException; + + private void resetCounters() { + totalSegments = C.LENGTH_UNSET; + downloadedSegments = C.LENGTH_UNSET; + downloadedBytes = C.LENGTH_UNSET; + } + + private void remove(Uri uri) { + CacheUtil.remove(cache, CacheUtil.generateKey(uri)); + } + + private void notifyListener(ProgressListener listener) { + if (listener != null) { + listener.onDownloadProgress(this, getDownloadPercentage(), downloadedBytes); + } + } + + /** + * Initializes totalSegments, downloadedSegments and downloadedBytes for selected representations. + * If not offline then downloads missing metadata. + * + * @return A list of not fully downloaded segments. + */ + private synchronized List initStatus(boolean offline) + throws IOException, InterruptedException { + DataSource dataSource = getDataSource(offline); + List segments = keys != null && keys.length > 0 + ? getSegments(dataSource, manifest, keys, offline) + : getAllSegments(dataSource, manifest, offline); + CachingCounters cachingCounters = new CachingCounters(); + totalSegments = segments.size(); + downloadedSegments = 0; + downloadedBytes = 0; + for (int i = segments.size() - 1; i >= 0; i--) { + Segment segment = segments.get(i); + CacheUtil.getCached(segment.dataSpec, cache, cachingCounters); + downloadedBytes += cachingCounters.alreadyCachedBytes; + if (cachingCounters.alreadyCachedBytes == cachingCounters.contentLength) { + // The segment is fully downloaded. + downloadedSegments++; + segments.remove(i); + } + } + return segments; + } + + private M getManifestIfNeeded(boolean offline) throws IOException { + if (manifest == null) { + manifest = getManifest(getDataSource(offline), manifestUri); + } + return manifest; + } + + private DataSource getDataSource(boolean offline) { + return offline ? offlineDataSource : dataSource; + } + +} 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 42ac938677..9c2be39576 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 @@ -32,12 +32,14 @@ import com.google.android.exoplayer2.Timeline; } @Override - public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int nextWindowIndexInChild = getTimelineByChildIndex(childIndex).getNextWindowIndex( windowIndex - firstWindowIndexInChild, - repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode); + repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, + shuffleModeEnabled); if (nextWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + nextWindowIndexInChild; } else { @@ -53,12 +55,14 @@ import com.google.android.exoplayer2.Timeline; } @Override - public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int previousWindowIndexInChild = getTimelineByChildIndex(childIndex).getPreviousWindowIndex( windowIndex - firstWindowIndexInChild, - repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode); + repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, + shuffleModeEnabled); if (previousWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + previousWindowIndexInChild; } else { 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 5d2bbcc33e..0f6f1b345d 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 @@ -198,19 +198,21 @@ public final class ConcatenatingMediaSource implements MediaSource { } @Override - public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { if (isRepeatOneAtomic && repeatMode == Player.REPEAT_MODE_ONE) { repeatMode = Player.REPEAT_MODE_ALL; } - return super.getNextWindowIndex(windowIndex, repeatMode); + return super.getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); } @Override - public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { if (isRepeatOneAtomic && repeatMode == Player.REPEAT_MODE_ONE) { repeatMode = Player.REPEAT_MODE_ALL; } - return super.getPreviousWindowIndex(windowIndex, repeatMode); + return super.getPreviousWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java index 4203abbf39..cfa5cec387 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java @@ -35,13 +35,25 @@ public abstract class ForwardingTimeline extends Timeline { } @Override - public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { - return timeline.getNextWindowIndex(windowIndex, repeatMode); + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { + return timeline.getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); } @Override - public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { - return timeline.getPreviousWindowIndex(windowIndex, repeatMode); + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { + return timeline.getPreviousWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); + } + + @Override + public int getLastWindowIndex(boolean shuffleModeEnabled) { + return timeline.getLastWindowIndex(shuffleModeEnabled); + } + + @Override + public int getFirstWindowIndex(boolean shuffleModeEnabled) { + return timeline.getFirstWindowIndex(shuffleModeEnabled); } @Override 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 1795fe8045..1c9c181914 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 @@ -167,14 +167,18 @@ public final class LoopingMediaSource implements MediaSource { } @Override - public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { - int childNextWindowIndex = timeline.getNextWindowIndex(windowIndex, repeatMode); + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { + int childNextWindowIndex = timeline.getNextWindowIndex(windowIndex, repeatMode, + shuffleModeEnabled); return childNextWindowIndex == C.INDEX_UNSET ? 0 : childNextWindowIndex; } @Override - public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { - int childPreviousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, repeatMode); + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { + int childPreviousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, repeatMode, + shuffleModeEnabled); return childPreviousWindowIndex == C.INDEX_UNSET ? getWindowCount() - 1 : childPreviousWindowIndex; } diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java new file mode 100644 index 0000000000..133bf19dba --- /dev/null +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java @@ -0,0 +1,80 @@ +/* + * 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.hls.offline; + +import com.google.android.exoplayer2.util.ClosedSource; + +/** + * Data for HLS downloading tests. + */ +@ClosedSource(reason = "Not ready yet") +/* package */ interface HlsDownloadTestData { + + String MASTER_PLAYLIST_URI = "test.m3u8"; + + String MEDIA_PLAYLIST_0_DIR = "gear0/"; + String MEDIA_PLAYLIST_0_URI = MEDIA_PLAYLIST_0_DIR + "prog_index.m3u8"; + String MEDIA_PLAYLIST_1_DIR = "gear1/"; + String MEDIA_PLAYLIST_1_URI = MEDIA_PLAYLIST_1_DIR + "prog_index.m3u8"; + String MEDIA_PLAYLIST_2_DIR = "gear2/"; + String MEDIA_PLAYLIST_2_URI = MEDIA_PLAYLIST_2_DIR + "prog_index.m3u8"; + String MEDIA_PLAYLIST_3_DIR = "gear3/"; + String MEDIA_PLAYLIST_3_URI = MEDIA_PLAYLIST_3_DIR + "prog_index.m3u8"; + + byte[] MASTER_PLAYLIST_DATA = + ("#EXTM3U\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=232370,CODECS=\"mp4a.40.2, avc1.4d4015\"\n" + + MEDIA_PLAYLIST_1_URI + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=649879,CODECS=\"mp4a.40.2, avc1.4d401e\"\n" + + MEDIA_PLAYLIST_2_URI + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=991714,CODECS=\"mp4a.40.2, avc1.4d401e\"\n" + + MEDIA_PLAYLIST_3_URI + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS=\"mp4a.40.2\"\n" + + MEDIA_PLAYLIST_0_URI).getBytes(); + + byte[] MEDIA_PLAYLIST_DATA = + ("#EXTM3U\n" + + "#EXT-X-TARGETDURATION:10\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXT-X-PLAYLIST-TYPE:VOD\n" + + "#EXTINF:9.97667,\n" + + "fileSequence0.ts\n" + + "#EXTINF:9.97667,\n" + + "fileSequence1.ts\n" + + "#EXTINF:9.97667,\n" + + "fileSequence2.ts\n" + + "#EXT-X-ENDLIST").getBytes(); + + String ENC_MEDIA_PLAYLIST_URI = "enc_index.m3u8"; + + byte[] ENC_MEDIA_PLAYLIST_DATA = + ("#EXTM3U\n" + + "#EXT-X-TARGETDURATION:10\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXT-X-PLAYLIST-TYPE:VOD\n" + + "#EXT-X-KEY:METHOD=AES-128,URI=\"enc.key\"\n" + + "#EXTINF:9.97667,\n" + + "fileSequence0.ts\n" + + "#EXTINF:9.97667,\n" + + "fileSequence1.ts\n" + + "#EXT-X-KEY:METHOD=AES-128,URI=\"enc2.key\"\n" + + "#EXTINF:9.97667,\n" + + "fileSequence2.ts\n" + + "#EXT-X-ENDLIST").getBytes(); + +} diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java new file mode 100644 index 0000000000..28afe450eb --- /dev/null +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java @@ -0,0 +1,210 @@ +/* + * 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.hls.offline; + +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.ENC_MEDIA_PLAYLIST_DATA; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.ENC_MEDIA_PLAYLIST_URI; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_PLAYLIST_DATA; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_PLAYLIST_URI; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_0_DIR; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_0_URI; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_1_DIR; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_1_URI; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_2_DIR; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_2_URI; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_3_DIR; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_3_URI; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_DATA; +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.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import com.google.android.exoplayer2.testutil.FakeDataSet; +import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; +import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.Util; +import java.io.File; + +/** Unit tests for {@link HlsDownloader}. */ +@ClosedSource(reason = "Not ready yet") +public class HlsDownloaderTest extends InstrumentationTestCase { + + private SimpleCache cache; + private File tempFolder; + private FakeDataSet fakeDataSet; + private HlsDownloader hlsDownloader; + + @Override + public void setUp() throws Exception { + super.setUp(); + tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); + + fakeDataSet = new FakeDataSet() + .setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA) + .setData(MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_DATA) + .setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", 10) + .setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", 11) + .setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts", 12) + .setData(MEDIA_PLAYLIST_2_URI, MEDIA_PLAYLIST_DATA) + .setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence0.ts", 13) + .setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence1.ts", 14) + .setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts", 15); + hlsDownloader = getHlsDownloader(MASTER_PLAYLIST_URI); + } + + @Override + public void tearDown() throws Exception { + Util.recursiveDelete(tempFolder); + super.tearDown(); + } + + public void testDownloadManifest() throws Exception { + HlsMasterPlaylist manifest = hlsDownloader.getManifest(); + + assertNotNull(manifest); + assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI); + } + + public void testSelectRepresentationsClearsPreviousSelection() throws Exception { + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_2_URI}); + hlsDownloader.download(null); + + assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI, MEDIA_PLAYLIST_2_URI, + MEDIA_PLAYLIST_2_DIR + "fileSequence0.ts", + MEDIA_PLAYLIST_2_DIR + "fileSequence1.ts", + MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts"); + } + + public void testCounterMethods() throws Exception { + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); + hlsDownloader.download(null); + + assertEquals(4, hlsDownloader.getTotalSegments()); + assertEquals(4, hlsDownloader.getDownloadedSegments()); + assertEquals(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12, hlsDownloader.getDownloadedBytes()); + } + + public void testInitStatus() throws Exception { + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); + hlsDownloader.download(null); + + HlsDownloader newHlsDownloader = + getHlsDownloader(MASTER_PLAYLIST_URI); + newHlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); + newHlsDownloader.init(); + + assertEquals(4, newHlsDownloader.getTotalSegments()); + assertEquals(4, newHlsDownloader.getDownloadedSegments()); + assertEquals(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12, newHlsDownloader.getDownloadedBytes()); + } + + public void testDownloadRepresentation() throws Exception { + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); + hlsDownloader.download(null); + + assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI, MEDIA_PLAYLIST_1_URI, + MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"); + } + + public void testDownloadMultipleRepresentations() throws Exception { + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_2_URI}); + hlsDownloader.download(null); + + assertCachedData(cache, fakeDataSet); + } + + public void testDownloadAllRepresentations() throws Exception { + // Add data for the rest of the playlists + fakeDataSet.setData(MEDIA_PLAYLIST_0_URI, MEDIA_PLAYLIST_DATA) + .setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence0.ts", 10) + .setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence1.ts", 11) + .setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence2.ts", 12) + .setData(MEDIA_PLAYLIST_3_URI, MEDIA_PLAYLIST_DATA) + .setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence0.ts", 13) + .setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence1.ts", 14) + .setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence2.ts", 15); + hlsDownloader = getHlsDownloader(MASTER_PLAYLIST_URI); + + // hlsDownloader.selectRepresentations() isn't called + hlsDownloader.download(null); + assertCachedData(cache, fakeDataSet); + hlsDownloader.remove(); + + // select something random + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); + // clear selection + hlsDownloader.selectRepresentations(null); + hlsDownloader.download(null); + assertCachedData(cache, fakeDataSet); + hlsDownloader.remove(); + + hlsDownloader.selectRepresentations(new String[0]); + hlsDownloader.download(null); + assertCachedData(cache, fakeDataSet); + hlsDownloader.remove(); + } + + public void testRemoveAll() throws Exception { + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_2_URI}); + hlsDownloader.download(null); + hlsDownloader.remove(); + + assertCacheEmpty(cache); + } + + public void testDownloadMediaPlaylist() throws Exception { + hlsDownloader = getHlsDownloader(MEDIA_PLAYLIST_1_URI); + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); + hlsDownloader.download(null); + + assertCachedData(cache, fakeDataSet, MEDIA_PLAYLIST_1_URI, + MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"); + } + + public void testDownloadEncMediaPlaylist() throws Exception { + fakeDataSet = new FakeDataSet() + .setData(ENC_MEDIA_PLAYLIST_URI, ENC_MEDIA_PLAYLIST_DATA) + .setRandomData("enc.key", 8) + .setRandomData("enc2.key", 9) + .setRandomData("fileSequence0.ts", 10) + .setRandomData("fileSequence1.ts", 11) + .setRandomData("fileSequence2.ts", 12); + hlsDownloader = + getHlsDownloader(ENC_MEDIA_PLAYLIST_URI); + hlsDownloader.selectRepresentations(new String[] {ENC_MEDIA_PLAYLIST_URI}); + hlsDownloader.download(null); + + assertCachedData(cache, fakeDataSet); + } + + private HlsDownloader getHlsDownloader(String mediaPlaylistUri) { + Factory factory = new Factory(null).setFakeDataSet(fakeDataSet); + return new HlsDownloader(Uri.parse(mediaPlaylistUri), + new DownloaderConstructorHelper(cache, factory)); + } + +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java new file mode 100644 index 0000000000..3c23e25796 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java @@ -0,0 +1,83 @@ +/* + * 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.hls.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.SegmentDownloadAction; +import com.google.android.exoplayer2.util.ClosedSource; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** An action to download or remove downloaded HLS streams. */ +@ClosedSource(reason = "Not ready yet") +public final class HlsDownloadAction extends SegmentDownloadAction { + + public static final Deserializer DESERIALIZER = new SegmentDownloadActionDeserializer() { + + @Override + public String getType() { + return TYPE; + } + + @Override + protected String readKey(DataInputStream input) throws IOException { + return input.readUTF(); + } + + @Override + protected String[] createKeyArray(int keyCount) { + return new String[0]; + } + + @Override + protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, + String[] keys) { + return new HlsDownloadAction(manifestUri, removeAction, keys); + } + + }; + + private static final String TYPE = "HlsDownloadAction"; + + /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, Object[]) */ + public HlsDownloadAction(Uri manifestUri, boolean removeAction, String... keys) { + super(manifestUri, removeAction, keys); + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public HlsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) + throws IOException { + HlsDownloader downloader = new HlsDownloader(manifestUri, constructorHelper); + if (!isRemoveAction()) { + downloader.selectRepresentations(keys); + } + return downloader; + } + + @Override + protected void writeKey(DataOutputStream output, String key) throws IOException { + output.writeUTF(key); + } + +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java new file mode 100644 index 0000000000..488b85e78a --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java @@ -0,0 +1,139 @@ +/* + * 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.hls.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.SegmentDownloader; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; +import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.UriUtil; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +/** + * Helper class to download HLS streams. + * + * A subset of renditions can be downloaded by selecting them using {@link + * #selectRepresentations(Object[])}. As key, string form of the rendition's url is used. The urls + * can be absolute or relative to the master playlist url. + */ +@ClosedSource(reason = "Not ready yet") +public final class HlsDownloader extends SegmentDownloader { + + /** + * @see SegmentDownloader#SegmentDownloader(Uri, DownloaderConstructorHelper) + */ + public HlsDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) { + super(manifestUri, constructorHelper); + } + + @Override + protected HlsMasterPlaylist getManifest(DataSource dataSource, Uri uri) throws IOException { + HlsPlaylist hlsPlaylist = loadManifest(dataSource, uri); + if (hlsPlaylist instanceof HlsMasterPlaylist) { + return (HlsMasterPlaylist) hlsPlaylist; + } else { + return HlsMasterPlaylist.createSingleVariantMasterPlaylist(hlsPlaylist.baseUri); + } + } + + @Override + protected List getAllSegments(DataSource dataSource, HlsMasterPlaylist manifest, + boolean allowIndexLoadErrors) throws InterruptedException, IOException { + ArrayList urls = new ArrayList<>(); + extractUrls(manifest.variants, urls); + extractUrls(manifest.audios, urls); + extractUrls(manifest.subtitles, urls); + return getSegments(dataSource, manifest, urls.toArray(new String[urls.size()]), + allowIndexLoadErrors); + } + + @Override + protected List getSegments(DataSource dataSource, HlsMasterPlaylist manifest, + String[] keys, boolean allowIndexLoadErrors) throws InterruptedException, IOException { + HashSet encryptionKeyUris = new HashSet<>(); + ArrayList segments = new ArrayList<>(); + for (String playlistUrl : keys) { + HlsMediaPlaylist mediaPlaylist = null; + Uri uri = UriUtil.resolveToUri(manifest.baseUri, playlistUrl); + try { + mediaPlaylist = (HlsMediaPlaylist) loadManifest(dataSource, uri); + } catch (IOException e) { + if (!allowIndexLoadErrors) { + throw e; + } + } + segments.add(new Segment(mediaPlaylist != null ? mediaPlaylist.startTimeUs : Long.MIN_VALUE, + new DataSpec(uri))); + if (mediaPlaylist == null) { + continue; + } + + HlsMediaPlaylist.Segment initSegment = mediaPlaylist.initializationSegment; + if (initSegment != null) { + addSegment(segments, mediaPlaylist, initSegment, encryptionKeyUris); + } + + List hlsSegments = mediaPlaylist.segments; + for (int i = 0; i < hlsSegments.size(); i++) { + addSegment(segments, mediaPlaylist, hlsSegments.get(i), encryptionKeyUris); + } + } + return segments; + } + + private HlsPlaylist loadManifest(DataSource dataSource, Uri uri) throws IOException { + DataSpec dataSpec = new DataSpec(uri, + DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH | DataSpec.FLAG_ALLOW_GZIP); + ParsingLoadable loadable = new ParsingLoadable<>(dataSource, dataSpec, + C.DATA_TYPE_MANIFEST, new HlsPlaylistParser()); + loadable.load(); + return loadable.getResult(); + } + + private static void addSegment(ArrayList segments, HlsMediaPlaylist mediaPlaylist, + HlsMediaPlaylist.Segment hlsSegment, HashSet encryptionKeyUris) + throws IOException, InterruptedException { + long startTimeUs = mediaPlaylist.startTimeUs + hlsSegment.relativeStartTimeUs; + if (hlsSegment.isEncrypted) { + Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, hlsSegment.encryptionKeyUri); + if (encryptionKeyUris.add(keyUri)) { + segments.add(new Segment(startTimeUs, new DataSpec(keyUri))); + } + } + Uri resolvedUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, hlsSegment.url); + segments.add(new Segment(startTimeUs, + new DataSpec(resolvedUri, hlsSegment.byterangeOffset, hlsSegment.byterangeLength, null))); + } + + private static void extractUrls(List hlsUrls, ArrayList urls) { + for (int i = 0; i < hlsUrls.size(); i++) { + urls.add(hlsUrls.get(i).url); + } + } + +} diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java new file mode 100644 index 0000000000..7478062ef8 --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java @@ -0,0 +1,86 @@ +/* + * 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.smoothstreaming.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.SegmentDownloadAction; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; +import com.google.android.exoplayer2.util.ClosedSource; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** An action to download or remove downloaded SmoothStreaming streams. */ +@ClosedSource(reason = "Not ready yet") +public final class SsDownloadAction extends SegmentDownloadAction { + + public static final Deserializer DESERIALIZER = + new SegmentDownloadActionDeserializer() { + + @Override + public String getType() { + return TYPE; + } + + @Override + protected TrackKey readKey(DataInputStream input) throws IOException { + return new TrackKey(input.readInt(), input.readInt()); + } + + @Override + protected TrackKey[] createKeyArray(int keyCount) { + return new TrackKey[keyCount]; + } + + @Override + protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, + TrackKey[] keys) { + return new SsDownloadAction(manifestUri, removeAction, keys); + } + + }; + + private static final String TYPE = "SsDownloadAction"; + + /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, Object[]) */ + public SsDownloadAction(Uri manifestUri, boolean removeAction, TrackKey... keys) { + super(manifestUri, removeAction, keys); + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public SsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) + throws IOException { + SsDownloader downloader = new SsDownloader(manifestUri, constructorHelper); + if (!isRemoveAction()) { + downloader.selectRepresentations(keys); + } + return downloader; + } + + @Override + protected void writeKey(DataOutputStream output, TrackKey key) throws IOException { + output.writeInt(key.streamElementIndex); + output.writeInt(key.trackIndex); + } + +} diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java new file mode 100644 index 0000000000..fe9c21d855 --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java @@ -0,0 +1,111 @@ +/* + * 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.smoothstreaming.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.SegmentDownloader; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.ClosedSource; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class to download SmoothStreaming streams. + * + *

Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link + * #getDownloadedBytes()}, this class isn't thread safe. + * + *

Example usage: + * + *

+ * {@code
+ * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor());
+ * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
+ * DownloaderConstructorHelper constructorHelper =
+ *     new DownloaderConstructorHelper(cache, factory);
+ * SsDownloader ssDownloader = new SsDownloader(manifestUrl, constructorHelper);
+ * // Select the first track of the first stream element
+ * ssDownloader.selectRepresentations(new TrackKey[] {new TrackKey(0, 0)});
+ * ssDownloader.download(new ProgressListener() {
+ *   @Override
+ *   public void onDownloadProgress(Downloader downloader, float downloadPercentage,
+ *       long downloadedBytes) {
+ *     // Invoked periodically during the download.
+ *   }
+ * });
+ * // Access downloaded data using CacheDataSource
+ * CacheDataSource cacheDataSource =
+ *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);}
+ * 
+ */ +@ClosedSource(reason = "Not ready yet") +public final class SsDownloader extends SegmentDownloader { + + /** + * @see SegmentDownloader#SegmentDownloader(Uri, DownloaderConstructorHelper) + */ + public SsDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) { + super(manifestUri, constructorHelper); + } + + @Override + public SsManifest getManifest(DataSource dataSource, Uri uri) throws IOException { + DataSpec dataSpec = new DataSpec(uri, + DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH | DataSpec.FLAG_ALLOW_GZIP); + ParsingLoadable loadable = new ParsingLoadable<>(dataSource, dataSpec, + C.DATA_TYPE_MANIFEST, new SsManifestParser()); + loadable.load(); + return loadable.getResult(); + } + + @Override + protected List getAllSegments(DataSource dataSource, SsManifest manifest, + boolean allowIndexLoadErrors) throws InterruptedException, IOException { + ArrayList segments = new ArrayList<>(); + for (int i = 0; i < manifest.streamElements.length; i++) { + StreamElement streamElement = manifest.streamElements[i]; + for (int j = 0; j < streamElement.formats.length; j++) { + segments.addAll(getSegments(dataSource, manifest, new TrackKey[] {new TrackKey(i, j)}, + allowIndexLoadErrors)); + } + } + return segments; + } + + @Override + protected List getSegments(DataSource dataSource, SsManifest manifest, + TrackKey[] keys, boolean allowIndexLoadErrors) throws InterruptedException, IOException { + ArrayList segments = new ArrayList<>(); + for (TrackKey key : keys) { + StreamElement streamElement = manifest.streamElements[key.streamElementIndex]; + for (int i = 0; i < streamElement.chunkCount; i++) { + segments.add(new Segment(streamElement.getStartTimeUs(i), + new DataSpec(streamElement.buildRequestUri(key.trackIndex, i)))); + } + } + return segments; + } + +} 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 3e6b5bf158..acb6e3e7cd 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 @@ -675,9 +675,11 @@ public class PlaybackControlView extends FrameLayout { timeline.getWindow(windowIndex, window); isSeekable = window.isSeekable; enablePrevious = isSeekable || !window.isDynamic - || timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode()) != C.INDEX_UNSET; + || timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode(), false) + != C.INDEX_UNSET; enableNext = window.isDynamic - || timeline.getNextWindowIndex(windowIndex, player.getRepeatMode()) != C.INDEX_UNSET; + || timeline.getNextWindowIndex(windowIndex, player.getRepeatMode(), false) + != C.INDEX_UNSET; if (player.isPlayingAd()) { // Always hide player controls during ads. hide(); @@ -861,7 +863,8 @@ public class PlaybackControlView extends FrameLayout { } int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); - int previousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode()); + int previousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode(), + false); if (previousWindowIndex != C.INDEX_UNSET && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS || (window.isDynamic && !window.isSeekable))) { @@ -877,7 +880,7 @@ public class PlaybackControlView extends FrameLayout { return; } int windowIndex = player.getCurrentWindowIndex(); - int nextWindowIndex = timeline.getNextWindowIndex(windowIndex, player.getRepeatMode()); + int nextWindowIndex = timeline.getNextWindowIndex(windowIndex, player.getRepeatMode(), false); if (nextWindowIndex != C.INDEX_UNSET) { seekTo(nextWindowIndex, C.TIME_UNSET); } else if (timeline.getWindow(windowIndex, window, false).isDynamic) { diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java new file mode 100644 index 0000000000..706dd72166 --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java @@ -0,0 +1,192 @@ +/* + * 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.net.Uri; +import android.test.ActivityInstrumentationTestCase2; +import com.google.android.exoplayer2.offline.Downloader; +import com.google.android.exoplayer2.offline.Downloader.ProgressListener; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +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.Representation; +import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import com.google.android.exoplayer2.source.dash.offline.DashDownloader; +import com.google.android.exoplayer2.testutil.HostActivity; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; +import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.Util; +import java.io.File; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Tests downloaded DASH playbacks. + */ +@ClosedSource(reason = "Not ready yet") +public final class DashDownloadTest extends ActivityInstrumentationTestCase2 { + + private static final String TAG = "DashDownloadTest"; + + private DashTestRunner testRunner; + private File tempFolder; + private SimpleCache cache; + + public DashDownloadTest() { + super(HostActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + testRunner = new DashTestRunner(TAG, getActivity(), getInstrumentation()) + .setManifestUrl(DashTestData.H264_MANIFEST) + .setFullPlaybackNoSeeking(true) + .setCanIncludeAdditionalVideoFormats(false) + .setAudioVideoFormats(DashTestData.AAC_AUDIO_REPRESENTATION_ID, + DashTestData.H264_CDD_FIXED); + tempFolder = Util.createTempDirectory(getActivity(), "ExoPlayerTest"); + cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); + } + + @Override + protected void tearDown() throws Exception { + testRunner = null; + Util.recursiveDelete(tempFolder); + cache = null; + super.tearDown(); + } + + // Download tests + + public void testDownload() throws Exception { + if (Util.SDK_INT < 16) { + return; // Pass. + } + + // Download manifest only + createDashDownloader(false).getManifest(); + long manifestLength = cache.getCacheSpace(); + + // Download representations + DashDownloader dashDownloader = downloadContent(false, Float.NaN); + assertEquals(cache.getCacheSpace() - manifestLength, dashDownloader.getDownloadedBytes()); + + testRunner.setStreamName("test_h264_fixed_download"). + setDataSourceFactory(newOfflineCacheDataSourceFactory()).run(); + + dashDownloader.remove(); + + assertEquals("There should be no content left.", 0, cache.getKeys().size()); + assertEquals("There should be no content left.", 0, cache.getCacheSpace()); + } + + public void testPartialDownload() throws Exception { + if (Util.SDK_INT < 16) { + return; // Pass. + } + + // Just download the first half and manifest + downloadContent(false, 0.5f); + + // Download the rest + DashDownloader dashDownloader = downloadContent(false, Float.NaN); + long downloadedBytes = dashDownloader.getDownloadedBytes(); + + // Make sure it doesn't download any data + dashDownloader = downloadContent(true, Float.NaN); + assertEquals(downloadedBytes, dashDownloader.getDownloadedBytes()); + + testRunner.setStreamName("test_h264_fixed_partial_download") + .setDataSourceFactory(newOfflineCacheDataSourceFactory()).run(); + } + + private DashDownloader downloadContent(boolean offline, float stopAt) throws Exception { + DashDownloader dashDownloader = createDashDownloader(offline); + DashManifest dashManifest = dashDownloader.getManifest(); + try { + ArrayList keys = new ArrayList<>(); + for (int pIndex = 0; pIndex < dashManifest.getPeriodCount(); pIndex++) { + List adaptationSets = dashManifest.getPeriod(pIndex).adaptationSets; + for (int aIndex = 0; aIndex < adaptationSets.size(); aIndex++) { + AdaptationSet adaptationSet = adaptationSets.get(aIndex); + List representations = adaptationSet.representations; + for (int rIndex = 0; rIndex < representations.size(); rIndex++) { + String id = representations.get(rIndex).format.id; + if (DashTestData.AAC_AUDIO_REPRESENTATION_ID.equals(id) + || DashTestData.H264_CDD_FIXED.equals(id)) { + keys.add(new RepresentationKey(pIndex, aIndex, rIndex)); + } + } + } + dashDownloader.selectRepresentations(keys.toArray(new RepresentationKey[keys.size()])); + TestProgressListener listener = new TestProgressListener(stopAt); + dashDownloader.download(listener); + } + } catch (InterruptedException e) { + // do nothing + } catch (IOException e) { + Throwable exception = e; + while (!(exception instanceof InterruptedIOException)) { + if (exception == null) { + throw e; + } + exception = exception.getCause(); + } + // else do nothing + } + return dashDownloader; + } + + private DashDownloader createDashDownloader(boolean offline) { + DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper(cache, + offline ? DummyDataSource.FACTORY : new DefaultHttpDataSourceFactory("ExoPlayer", null)); + return new DashDownloader(Uri.parse(DashTestData.H264_MANIFEST), constructorHelper); + } + + private CacheDataSourceFactory newOfflineCacheDataSourceFactory() { + return new CacheDataSourceFactory(cache, DummyDataSource.FACTORY, + CacheDataSource.FLAG_BLOCK_ON_CACHE); + } + + private static class TestProgressListener implements ProgressListener { + + private float stopAt; + + private TestProgressListener(float stopAt) { + this.stopAt = stopAt; + } + + @Override + public void onDownloadProgress(Downloader downloader, float downloadPercentage, + long downloadedBytes) { + System.out.printf("onDownloadProgress downloadPercentage = [%g], downloadedData = [%d]%n", + downloadPercentage, downloadedBytes); + if (downloadPercentage >= stopAt) { + Thread.currentThread().interrupt(); + } + } + + } + +} 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 8357ce70c7..36d72db48b 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 @@ -74,7 +74,7 @@ public final class TimelineAsserts { @Player.RepeatMode int repeatMode, int... expectedPreviousWindowIndices) { for (int i = 0; i < timeline.getWindowCount(); i++) { assertEquals(expectedPreviousWindowIndices[i], - timeline.getPreviousWindowIndex(i, repeatMode)); + timeline.getPreviousWindowIndex(i, repeatMode, false)); } } @@ -86,14 +86,14 @@ public final class TimelineAsserts { int... expectedNextWindowIndices) { for (int i = 0; i < timeline.getWindowCount(); i++) { assertEquals(expectedNextWindowIndices[i], - timeline.getNextWindowIndex(i, repeatMode)); + timeline.getNextWindowIndex(i, repeatMode, false)); } } /** * 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)}. + * asserts the correct behavior of {@link Timeline#getNextWindowIndex(int, int, boolean)}. */ public static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCounts) { int windowCount = timeline.getWindowCount(); @@ -119,16 +119,19 @@ public final class TimelineAsserts { } assertEquals(expectedWindowIndex, period.windowIndex); if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) { - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_OFF)); - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ONE)); - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ALL)); + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_OFF, + false)); + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ONE, + false)); + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ALL, + false)); } else { int nextWindowOff = timeline.getNextWindowIndex(expectedWindowIndex, - Player.REPEAT_MODE_OFF); + Player.REPEAT_MODE_OFF, false); int nextWindowOne = timeline.getNextWindowIndex(expectedWindowIndex, - Player.REPEAT_MODE_ONE); + Player.REPEAT_MODE_ONE, false); int nextWindowAll = timeline.getNextWindowIndex(expectedWindowIndex, - Player.REPEAT_MODE_ALL); + Player.REPEAT_MODE_ALL, false); int nextPeriodOff = nextWindowOff == C.INDEX_UNSET ? C.INDEX_UNSET : accumulatedPeriodCounts[nextWindowOff]; int nextPeriodOne = nextWindowOne == C.INDEX_UNSET ? C.INDEX_UNSET @@ -136,11 +139,11 @@ public final class TimelineAsserts { int nextPeriodAll = nextWindowAll == C.INDEX_UNSET ? C.INDEX_UNSET : accumulatedPeriodCounts[nextWindowAll]; assertEquals(nextPeriodOff, timeline.getNextPeriodIndex(i, period, window, - Player.REPEAT_MODE_OFF)); + Player.REPEAT_MODE_OFF, false)); assertEquals(nextPeriodOne, timeline.getNextPeriodIndex(i, period, window, - Player.REPEAT_MODE_ONE)); + Player.REPEAT_MODE_ONE, false)); assertEquals(nextPeriodAll, timeline.getNextPeriodIndex(i, period, window, - Player.REPEAT_MODE_ALL)); + Player.REPEAT_MODE_ALL, false)); } } } From 2c8d5f846e4c0e01b772c46f9626954a836a9a47 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 03:27:40 -0700 Subject: [PATCH 0005/1327] Pass shuffle mode to timeline assertion helper methods. This allows to test the expected behaviour of timeline with different shuffle modes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166181091 --- .../android/exoplayer2/TimelineTest.java | 26 ++++--- .../source/ClippingMediaSourceTest.java | 15 ++-- .../source/ConcatenatingMediaSourceTest.java | 76 ++++++++++--------- .../DynamicConcatenatingMediaSourceTest.java | 15 ++-- .../source/LoopingMediaSourceTest.java | 39 +++++----- .../exoplayer2/testutil/TimelineAsserts.java | 55 +++++--------- 6 files changed, 113 insertions(+), 113 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 d9ee27bd62..f5c33843a1 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 @@ -33,23 +33,25 @@ public class TimelineTest extends TestCase { Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(1, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); } public void testMultiPeriodTimeline() { Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(5, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 5); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 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 66b0337450..5e615dbc7f 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 @@ -109,13 +109,14 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); TimelineAsserts.assertWindowIds(clippedTimeline, 111); TimelineAsserts.assertPeriodCounts(clippedTimeline, 1); - TimelineAsserts.assertPreviousWindowIndices( - clippedTimeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, 0); - TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, false, 0); + TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, false, 0); } /** 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 3bf89f9bcc..34f44b47f7 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 @@ -34,22 +34,26 @@ public final class ConcatenatingMediaSourceTest extends TestCase { Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); } public void testMultipleMediaSources() { @@ -58,24 +62,26 @@ public final class ConcatenatingMediaSourceTest extends TestCase { Timeline timeline = getConcatenatedTimeline(false, timelines); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); timeline = getConcatenatedTimeline(true, timelines); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); } public void testNestedMediaSources() { @@ -84,14 +90,16 @@ public final class ConcatenatingMediaSourceTest extends TestCase { 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, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 3, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 3, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, 1, 2, 3, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 3, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 3, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, + 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, + 3, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, 3, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 3, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 3, 0); } /** 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 8d29a95d89..35233febf5 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 @@ -120,13 +120,14 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } // Assert correct next and previous indices behavior after some insertions and removals. - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); // Remove at front of queue. mediaSource.removeMediaSource(0); 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 d2045c29a5..c32f5cb624 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 @@ -42,30 +42,31 @@ public class LoopingMediaSourceTest extends TestCase { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); } public void testMultiLoop() { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3); 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, Player.REPEAT_MODE_OFF, + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 8, 0, 1, 2, 3, 4, 5, 6, 7); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, 1, 2, 3, 4, 5, 6, 7, 8, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 3, 4, 5, 6, 7, 8, 0); } @@ -73,12 +74,12 @@ public class LoopingMediaSourceTest extends TestCase { Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); } /** 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 36d72db48b..74129a0e69 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 @@ -67,26 +67,27 @@ public final class TimelineAsserts { } /** - * Asserts that previous window indices for each window are set correctly depending on the repeat - * mode. + * Asserts that previous window indices for each window depending on the repeat mode and the + * shuffle mode are equal to the given sequence. */ public static void assertPreviousWindowIndices(Timeline timeline, - @Player.RepeatMode int repeatMode, int... expectedPreviousWindowIndices) { + @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled, + int... expectedPreviousWindowIndices) { for (int i = 0; i < timeline.getWindowCount(); i++) { assertEquals(expectedPreviousWindowIndices[i], - timeline.getPreviousWindowIndex(i, repeatMode, false)); + timeline.getPreviousWindowIndex(i, repeatMode, shuffleModeEnabled)); } } /** - * Asserts that next window indices for each window are set correctly depending on the repeat - * mode. + * Asserts that next window indices for each window depending on the repeat mode and the + * shuffle mode are equal to the given sequence. */ public static void assertNextWindowIndices(Timeline timeline, @Player.RepeatMode int repeatMode, - int... expectedNextWindowIndices) { + boolean shuffleModeEnabled, int... expectedNextWindowIndices) { for (int i = 0; i < timeline.getWindowCount(); i++) { assertEquals(expectedNextWindowIndices[i], - timeline.getNextWindowIndex(i, repeatMode, false)); + timeline.getNextWindowIndex(i, repeatMode, shuffleModeEnabled)); } } @@ -118,34 +119,20 @@ public final class TimelineAsserts { expectedWindowIndex++; } assertEquals(expectedWindowIndex, period.windowIndex); - if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) { - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_OFF, - false)); - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ONE, - false)); - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ALL, - false)); - } else { - int nextWindowOff = timeline.getNextWindowIndex(expectedWindowIndex, - Player.REPEAT_MODE_OFF, false); - int nextWindowOne = timeline.getNextWindowIndex(expectedWindowIndex, - Player.REPEAT_MODE_ONE, false); - int nextWindowAll = timeline.getNextWindowIndex(expectedWindowIndex, - Player.REPEAT_MODE_ALL, false); - 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, - Player.REPEAT_MODE_OFF, false)); - assertEquals(nextPeriodOne, timeline.getNextPeriodIndex(i, period, window, - Player.REPEAT_MODE_ONE, false)); - assertEquals(nextPeriodAll, timeline.getNextPeriodIndex(i, period, window, - Player.REPEAT_MODE_ALL, false)); + for (@Player.RepeatMode int repeatMode + : new int[] { Player.REPEAT_MODE_OFF, Player.REPEAT_MODE_ONE, Player.REPEAT_MODE_ALL }) { + if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) { + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, repeatMode, false)); + } else { + int nextWindow = timeline.getNextWindowIndex(expectedWindowIndex, repeatMode, false); + int nextPeriod = nextWindow == C.INDEX_UNSET ? C.INDEX_UNSET + : accumulatedPeriodCounts[nextWindow]; + assertEquals(nextPeriod, timeline.getNextPeriodIndex(i, period, window, repeatMode, + false)); + } } } } } + From bd81181892879b3ca0a573b669cfbce14015cd24 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 03:29:21 -0700 Subject: [PATCH 0006/1327] Add shortcut methods to query next or previous window index. This functionality is most likely needed by UI modules which currently need to obtain the timeline, the current repeat and shuffle modes and are only then able to query the next/previous window index using this information. Adding these methods simplifies these cumbersome requests. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166181202 --- .../android/exoplayer2/ext/cast/CastPlayer.java | 10 ++++++++++ .../ext/mediasession/TimelineQueueNavigator.java | 7 +++---- .../DynamicConcatenatingMediaSourceTest.java | 10 ++++++++++ .../google/android/exoplayer2/ExoPlayerImpl.java | 12 ++++++++++++ .../java/com/google/android/exoplayer2/Player.java | 14 ++++++++++++++ .../google/android/exoplayer2/SimpleExoPlayer.java | 10 ++++++++++ .../android/exoplayer2/ui/PlaybackControlView.java | 13 +++++-------- .../exoplayer2/testutil/FakeSimpleExoPlayer.java | 10 ++++++++++ 8 files changed, 74 insertions(+), 12 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 50ae7ea5ba..e79fef74d5 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -371,6 +371,16 @@ public final class CastPlayer implements Player { return 0; } + @Override + public int getNextWindowIndex() { + return C.INDEX_UNSET; + } + + @Override + public int getPreviousWindowIndex() { + return C.INDEX_UNSET; + } + @Override public long getDuration() { return currentTimeline.isEmpty() ? C.TIME_UNSET diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index bd3f3f2820..9d7ed75c83 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -126,8 +126,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu if (timeline.isEmpty()) { return; } - int previousWindowIndex = timeline.getPreviousWindowIndex(player.getCurrentWindowIndex(), - player.getRepeatMode(), false); + int previousWindowIndex = player.getPreviousWindowIndex(); if (player.getCurrentPosition() > MAX_POSITION_FOR_SEEK_TO_PREVIOUS || previousWindowIndex == C.INDEX_UNSET) { player.seekTo(0); @@ -154,8 +153,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu if (timeline.isEmpty()) { return; } - int nextWindowIndex = timeline.getNextWindowIndex(player.getCurrentWindowIndex(), - player.getRepeatMode(), false); + int nextWindowIndex = player.getNextWindowIndex(); if (nextWindowIndex != C.INDEX_UNSET) { player.seekTo(nextWindowIndex, C.TIME_UNSET); } @@ -186,3 +184,4 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu } } + 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 35233febf5..9cdb461d7b 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 @@ -502,6 +502,16 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { throw new UnsupportedOperationException(); } + @Override + public int getNextWindowIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getPreviousWindowIndex() { + throw new UnsupportedOperationException(); + } + @Override public long getDuration() { throw new UnsupportedOperationException(); 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 0ce920a16f..7bf0cd5a02 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 @@ -317,6 +317,18 @@ import java.util.concurrent.CopyOnWriteArraySet; } } + @Override + public int getNextWindowIndex() { + return timeline.getNextWindowIndex(getCurrentWindowIndex(), getRepeatMode(), + getShuffleModeEnabled()); + } + + @Override + public int getPreviousWindowIndex() { + return timeline.getPreviousWindowIndex(getCurrentWindowIndex(), getRepeatMode(), + getShuffleModeEnabled()); + } + @Override public long getDuration() { if (timeline.isEmpty()) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 6eee930018..ae2785f6f8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -363,6 +363,20 @@ public interface Player { */ int getCurrentWindowIndex(); + /** + * Returns the index of the next timeline window to be played, which may depend on the current + * repeat mode and whether shuffle mode is enabled. Returns {@link C#INDEX_UNSET} if the window + * currently being played is the last window. + */ + int getNextWindowIndex(); + + /** + * Returns the index of the previous timeline window to be played, which may depend on the current + * repeat mode and whether shuffle mode is enabled. Returns {@link C#INDEX_UNSET} if the window + * currently being played is the first window. + */ + int getPreviousWindowIndex(); + /** * Returns the duration of the current window in milliseconds, or {@link C#TIME_UNSET} if the * duration is not known. 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 9fcc4d2128..1c35adb917 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 @@ -754,6 +754,16 @@ public class SimpleExoPlayer implements ExoPlayer { return player.getCurrentWindowIndex(); } + @Override + public int getNextWindowIndex() { + return player.getNextWindowIndex(); + } + + @Override + public int getPreviousWindowIndex() { + return player.getPreviousWindowIndex(); + } + @Override public long getDuration() { return player.getDuration(); 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 acb6e3e7cd..105dbc2495 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 @@ -675,11 +675,8 @@ public class PlaybackControlView extends FrameLayout { timeline.getWindow(windowIndex, window); isSeekable = window.isSeekable; enablePrevious = isSeekable || !window.isDynamic - || timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode(), false) - != C.INDEX_UNSET; - enableNext = window.isDynamic - || timeline.getNextWindowIndex(windowIndex, player.getRepeatMode(), false) - != C.INDEX_UNSET; + || player.getPreviousWindowIndex() != C.INDEX_UNSET; + enableNext = window.isDynamic || player.getNextWindowIndex() != C.INDEX_UNSET; if (player.isPlayingAd()) { // Always hide player controls during ads. hide(); @@ -863,8 +860,7 @@ public class PlaybackControlView extends FrameLayout { } int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); - int previousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode(), - false); + int previousWindowIndex = player.getPreviousWindowIndex(); if (previousWindowIndex != C.INDEX_UNSET && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS || (window.isDynamic && !window.isSeekable))) { @@ -880,7 +876,7 @@ public class PlaybackControlView extends FrameLayout { return; } int windowIndex = player.getCurrentWindowIndex(); - int nextWindowIndex = timeline.getNextWindowIndex(windowIndex, player.getRepeatMode(), false); + int nextWindowIndex = player.getNextWindowIndex(); if (nextWindowIndex != C.INDEX_UNSET) { seekTo(nextWindowIndex, C.TIME_UNSET); } else if (timeline.getWindow(windowIndex, window, false).isDynamic) { @@ -1146,3 +1142,4 @@ public class PlaybackControlView extends FrameLayout { } } + diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 7edaa6b13e..67a83b84e1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -250,6 +250,16 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { return 0; } + @Override + public int getNextWindowIndex() { + return C.INDEX_UNSET; + } + + @Override + public int getPreviousWindowIndex() { + return C.INDEX_UNSET; + } + @Override public long getDuration() { return C.usToMs(durationUs); From 4883a9ba9a44479d02d4c280648a213bdb741e47 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 05:59:32 -0700 Subject: [PATCH 0007/1327] Add shuffle support to infinitely looping timeline. In addition, let unit test assert window indices for both shuffle modes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166191069 --- .../source/LoopingMediaSourceTest.java | 64 +++++++++++-------- .../exoplayer2/source/LoopingMediaSource.java | 5 +- 2 files changed, 41 insertions(+), 28 deletions(-) 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 c32f5cb624..52c313ed47 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 @@ -42,44 +42,55 @@ public class LoopingMediaSourceTest extends TestCase { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); + for (boolean shuffled : new boolean[] { false, true }) { + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, + 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, + 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); + } } public void testMultiLoop() { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3); 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, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, - 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, - 8, 0, 1, 2, 3, 4, 5, 6, 7); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - 1, 2, 3, 4, 5, 6, 7, 8, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, - 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, - 1, 2, 3, 4, 5, 6, 7, 8, 0); + for (boolean shuffled : new boolean[] { false, true }) { + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, + 0, 1, 2, 3, 4, 5, 6, 7, 8); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, + 8, 0, 1, 2, 3, 4, 5, 6, 7); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + 1, 2, 3, 4, 5, 6, 7, 8, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, + 0, 1, 2, 3, 4, 5, 6, 7, 8); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, + 1, 2, 3, 4, 5, 6, 7, 8, 0); + } } public void testInfiniteLoop() { Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); + for (boolean shuffled : new boolean[] { false, true }) { + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, + 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, + 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); + } } /** @@ -93,3 +104,4 @@ public class LoopingMediaSourceTest extends TestCase { } } + 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 1c9c181914..28380e50b2 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 @@ -171,7 +171,8 @@ public final class LoopingMediaSource implements MediaSource { boolean shuffleModeEnabled) { int childNextWindowIndex = timeline.getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); - return childNextWindowIndex == C.INDEX_UNSET ? 0 : childNextWindowIndex; + return childNextWindowIndex == C.INDEX_UNSET ? getFirstWindowIndex(shuffleModeEnabled) + : childNextWindowIndex; } @Override @@ -179,7 +180,7 @@ public final class LoopingMediaSource implements MediaSource { boolean shuffleModeEnabled) { int childPreviousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); - return childPreviousWindowIndex == C.INDEX_UNSET ? getWindowCount() - 1 + return childPreviousWindowIndex == C.INDEX_UNSET ? getLastWindowIndex(shuffleModeEnabled) : childPreviousWindowIndex; } From 9fd19d0e7c20818269c488e8fdd4d17d9528e5e0 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 06:00:51 -0700 Subject: [PATCH 0008/1327] Add shuffle logic to concatenated timelines. The implementation in the abstract base class takes care to forward the queries to the correct methods given the shuffle mode and a given shuffle order. All concatenated timeline implementations use an unshuffled order so far. The handling of the shuffle orders will follow in other changes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166191165 --- .../source/AbstractConcatenatedTimeline.java | 63 +++++++++++++------ .../source/ConcatenatingMediaSource.java | 3 +- .../DynamicConcatenatingMediaSource.java | 3 +- .../exoplayer2/source/LoopingMediaSource.java | 3 +- 4 files changed, 50 insertions(+), 22 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 9c2be39576..07813ff046 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 @@ -26,9 +26,17 @@ import com.google.android.exoplayer2.Timeline; /* package */ abstract class AbstractConcatenatedTimeline extends Timeline { private final int childCount; + private final ShuffleOrder shuffleOrder; - public AbstractConcatenatedTimeline(int childCount) { - this.childCount = childCount; + /** + * Sets up a concatenated timeline with a shuffle order of child timelines. + * + * @param shuffleOrder A shuffle order of child timelines. The number of child timelines must + * match the number of elements in the shuffle order. + */ + public AbstractConcatenatedTimeline(ShuffleOrder shuffleOrder) { + this.shuffleOrder = shuffleOrder; + this.childCount = shuffleOrder.getLength(); } @Override @@ -42,16 +50,17 @@ import com.google.android.exoplayer2.Timeline; shuffleModeEnabled); if (nextWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + nextWindowIndexInChild; - } else { - int nextChildIndex = childIndex + 1; - if (nextChildIndex < childCount) { - return getFirstWindowIndexByChildIndex(nextChildIndex); - } else if (repeatMode == Player.REPEAT_MODE_ALL) { - return 0; - } else { - return C.INDEX_UNSET; - } } + int nextChildIndex = shuffleModeEnabled ? shuffleOrder.getNextIndex(childIndex) + : childIndex + 1; + if (nextChildIndex != C.INDEX_UNSET && nextChildIndex < childCount) { + return getFirstWindowIndexByChildIndex(nextChildIndex) + + getTimelineByChildIndex(nextChildIndex).getFirstWindowIndex(shuffleModeEnabled); + } + if (repeatMode == Player.REPEAT_MODE_ALL) { + return getFirstWindowIndex(shuffleModeEnabled); + } + return C.INDEX_UNSET; } @Override @@ -65,15 +74,31 @@ import com.google.android.exoplayer2.Timeline; shuffleModeEnabled); if (previousWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + previousWindowIndexInChild; - } else { - if (firstWindowIndexInChild > 0) { - return firstWindowIndexInChild - 1; - } else if (repeatMode == Player.REPEAT_MODE_ALL) { - return getWindowCount() - 1; - } else { - return C.INDEX_UNSET; - } } + int previousChildIndex = shuffleModeEnabled ? shuffleOrder.getPreviousIndex(childIndex) + : childIndex - 1; + if (previousChildIndex != C.INDEX_UNSET && previousChildIndex >= 0) { + return getFirstWindowIndexByChildIndex(previousChildIndex) + + getTimelineByChildIndex(previousChildIndex).getLastWindowIndex(shuffleModeEnabled); + } + if (repeatMode == Player.REPEAT_MODE_ALL) { + return getLastWindowIndex(shuffleModeEnabled); + } + return C.INDEX_UNSET; + } + + @Override + public int getLastWindowIndex(boolean shuffleModeEnabled) { + int lastChildIndex = shuffleModeEnabled ? shuffleOrder.getLastIndex() : childCount - 1; + return getFirstWindowIndexByChildIndex(lastChildIndex) + + getTimelineByChildIndex(lastChildIndex).getLastWindowIndex(shuffleModeEnabled); + } + + @Override + public int getFirstWindowIndex(boolean shuffleModeEnabled) { + int firstChildIndex = shuffleModeEnabled ? shuffleOrder.getFirstIndex() : 0; + return getFirstWindowIndexByChildIndex(firstChildIndex) + + getTimelineByChildIndex(firstChildIndex).getFirstWindowIndex(shuffleModeEnabled); } @Override 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 0f6f1b345d..f12e1c006f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -167,7 +168,7 @@ public final class ConcatenatingMediaSource implements MediaSource { private final boolean isRepeatOneAtomic; public ConcatenatedTimeline(Timeline[] timelines, boolean isRepeatOneAtomic) { - super(timelines.length); + super(new UnshuffledShuffleOrder(timelines.length)); int[] sourcePeriodOffsets = new int[timelines.length]; int[] sourceWindowOffsets = new int[timelines.length]; long periodCount = 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index c2d2e5f11e..02d4bad2bf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -23,6 +23,7 @@ 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.source.ShuffleOrder.UnshuffledShuffleOrder; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; @@ -397,7 +398,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl public ConcatenatedTimeline(Collection mediaSourceHolders, int windowCount, int periodCount) { - super(mediaSourceHolders.size()); + super(new UnshuffledShuffleOrder(mediaSourceHolders.size())); this.windowCount = windowCount; this.periodCount = periodCount; int childCount = mediaSourceHolders.size(); 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 28380e50b2..00e3c50506 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 @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -101,7 +102,7 @@ public final class LoopingMediaSource implements MediaSource { private final int loopCount; public LoopingTimeline(Timeline childTimeline, int loopCount) { - super(loopCount); + super(new UnshuffledShuffleOrder(loopCount)); this.childTimeline = childTimeline; childPeriodCount = childTimeline.getPeriodCount(); childWindowCount = childTimeline.getWindowCount(); From b27c25e88072007b39a7a3c283eed50c2d0efdcd Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 06:42:24 -0700 Subject: [PATCH 0009/1327] Add fake shuffle order. This just implements a reverse order which is different from the original order but still deterministic for simplified testing. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166194311 --- .../exoplayer2/testutil/FakeShuffleOrder.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java new file mode 100644 index 0000000000..0664f47023 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java @@ -0,0 +1,68 @@ +/* + * 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.source.ShuffleOrder; + +/** + * Fake {@link ShuffleOrder} which returns a reverse order. This order is thus deterministic but + * different from the original order. + */ +public final class FakeShuffleOrder implements ShuffleOrder { + + private final int length; + + public FakeShuffleOrder(int length) { + this.length = length; + } + + @Override + public int getLength() { + return length; + } + + @Override + public int getNextIndex(int index) { + return index > 0 ? index - 1 : C.INDEX_UNSET; + } + + @Override + public int getPreviousIndex(int index) { + return index < length - 1 ? index + 1 : C.INDEX_UNSET; + } + + @Override + public int getLastIndex() { + return length > 0 ? 0 : C.INDEX_UNSET; + } + + @Override + public int getFirstIndex() { + return length > 0 ? length - 1 : C.INDEX_UNSET; + } + + @Override + public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) { + return new FakeShuffleOrder(length + insertionCount); + } + + @Override + public ShuffleOrder cloneAndRemove(int removalIndex) { + return new FakeShuffleOrder(length - 1); + } + +} From 8115e11489cff49dec9cd15c208bc65fc2dfea41 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 23 Aug 2017 06:53:11 -0700 Subject: [PATCH 0010/1327] Allow subclasses to customize the MediaFormat Make getMediaFormat protected so that subclasses can set additional MediaFormat keys. For example, if the decoder output needs to be read back via an ImageReader as YUV data it is necessary to set KEY_COLOR_FORMAT to COLOR_FormatYUV420Flexible. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166195211 --- .../video/MediaCodecVideoRenderer.java | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 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 8fe3476351..c11c415cd7 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 @@ -737,28 +737,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return earlyUs < -30000; } - @SuppressLint("InlinedApi") - private static MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues, - boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) { - MediaFormat frameworkMediaFormat = format.getFrameworkMediaFormatV16(); - // Set the maximum adaptive video dimensions. - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width); - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height); - // Set the maximum input size. - if (codecMaxValues.inputSize != Format.NO_VALUE) { - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize); - } - // Set FRC workaround. - if (deviceNeedsAutoFrcWorkaround) { - frameworkMediaFormat.setInteger("auto-frc", 0); - } - // Configure tunneling if enabled. - if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { - configureTunnelingV21(frameworkMediaFormat, tunnelingAudioSessionId); - } - return frameworkMediaFormat; - } - @TargetApi(23) private static void setOutputSurfaceV23(MediaCodec codec, Surface surface) { codec.setOutputSurface(surface); @@ -814,6 +792,40 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); } + /** + * Returns the framework {@link MediaFormat} that should be used to configure the decoder when + * playing media in the specified input format. + * + * @param format The format of input media. + * @param codecMaxValues The codec's maximum supported values. + * @param deviceNeedsAutoFrcWorkaround Whether the device is known to enable frame-rate conversion + * logic that negatively impacts ExoPlayer. + * @param tunnelingAudioSessionId The audio session id to use for tunneling, or + * {@link C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled. + * @return The framework {@link MediaFormat} that should be used to configure the decoder. + */ + @SuppressLint("InlinedApi") + protected MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues, + boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) { + MediaFormat frameworkMediaFormat = format.getFrameworkMediaFormatV16(); + // Set the maximum adaptive video dimensions. + frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width); + frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height); + // Set the maximum input size. + if (codecMaxValues.inputSize != Format.NO_VALUE) { + frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize); + } + // Set FRC workaround. + if (deviceNeedsAutoFrcWorkaround) { + frameworkMediaFormat.setInteger("auto-frc", 0); + } + // Configure tunneling if enabled. + if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { + configureTunnelingV21(frameworkMediaFormat, tunnelingAudioSessionId); + } + return frameworkMediaFormat; + } + /** * Returns a maximum video size to use when configuring a codec for {@code format} in a way * that will allow possible adaptation to other compatible formats that are expected to have the From e15633e9060d409996ff0f98e4bf274d052e821f Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 07:15:41 -0700 Subject: [PATCH 0011/1327] Add shuffle support to ConcatenatingMediaSource. The media source is initialized with a DefaultShuffleOrder which can be changed at any time. This shuffle order is then used within the corresponding timeline. The isRepeatOneAtomic flag is extended to also suppress shuffling (now called isAtomic only). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166197184 --- .../source/ConcatenatingMediaSourceTest.java | 86 +++++++++++++------ .../source/ConcatenatingMediaSource.java | 58 +++++++++---- 2 files changed, 103 insertions(+), 41 deletions(-) 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 34f44b47f7..fd0acf2ab3 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,6 +19,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.TestUtil; @@ -34,26 +35,30 @@ public final class ConcatenatingMediaSourceTest extends TestCase { Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); + for (boolean shuffled : new boolean[] { false, true }) { + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); + } timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); + for (boolean shuffled : new boolean[] { false, true }) { + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); + } } public void testMultipleMediaSources() { @@ -70,18 +75,36 @@ public final class ConcatenatingMediaSourceTest extends TestCase { 1, 2, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); + assertEquals(0, timeline.getFirstWindowIndex(false)); + assertEquals(2, timeline.getLastWindowIndex(false)); + assertEquals(2, timeline.getFirstWindowIndex(true)); + assertEquals(0, timeline.getLastWindowIndex(true)); timeline = getConcatenatedTimeline(true, timelines); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); + for (boolean shuffled : new boolean[] { false, true }) { + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, + 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, + 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); + assertEquals(0, timeline.getFirstWindowIndex(shuffled)); + assertEquals(2, timeline.getLastWindowIndex(shuffled)); + } } public void testNestedMediaSources() { @@ -100,6 +123,16 @@ public final class ConcatenatingMediaSourceTest extends TestCase { 1, 2, 3, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 3, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 3, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + 1, 3, C.INDEX_UNSET, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, + 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, + 1, 3, 0, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + C.INDEX_UNSET, 0, 3, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 3, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 3, 1); } /** @@ -112,8 +145,9 @@ public final class ConcatenatingMediaSourceTest extends TestCase { for (int i = 0; i < timelines.length; i++) { mediaSources[i] = new FakeMediaSource(timelines[i], null); } - return TestUtil.extractTimelineFromMediaSource( - new ConcatenatingMediaSource(isRepeatOneAtomic, mediaSources)); + ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(isRepeatOneAtomic, + new FakeShuffleOrder(mediaSources.length), mediaSources); + return TestUtil.extractTimelineFromMediaSource(mediaSource); } private static FakeTimeline createFakeTimeline(int periodCount, int windowId) { 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 f12e1c006f..0c7bcece68 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -19,7 +19,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder; +import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -39,10 +39,11 @@ public final class ConcatenatingMediaSource implements MediaSource { private final Object[] manifests; private final Map sourceIndexByMediaPeriod; private final boolean[] duplicateFlags; - private final boolean isRepeatOneAtomic; + private final boolean isAtomic; private Listener listener; private ConcatenatedTimeline timeline; + private ShuffleOrder shuffleOrder; /** * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same @@ -53,17 +54,33 @@ public final class ConcatenatingMediaSource implements MediaSource { } /** - * @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 Player.REPEAT_MODE_ONE}. + * @param isAtomic Whether the concatenated media source shall be treated as atomic, + * i.e., treated as a single item for repeating and shuffling. * @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) { + public ConcatenatingMediaSource(boolean isAtomic, MediaSource... mediaSources) { + this(isAtomic, new DefaultShuffleOrder(mediaSources.length), mediaSources); + } + + /** + * @param isAtomic Whether the concatenated media source shall be treated as atomic, + * i.e., treated as a single item for repeating and shuffling. + * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. The + * number of elements in the shuffle order must match the number of concatenated + * {@link MediaSource}s. + * @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 isAtomic, ShuffleOrder shuffleOrder, + MediaSource... mediaSources) { for (MediaSource mediaSource : mediaSources) { Assertions.checkNotNull(mediaSource); } + Assertions.checkArgument(shuffleOrder.getLength() == mediaSources.length); this.mediaSources = mediaSources; - this.isRepeatOneAtomic = isRepeatOneAtomic; + this.isAtomic = isAtomic; + this.shuffleOrder = shuffleOrder; timelines = new Timeline[mediaSources.length]; manifests = new Object[mediaSources.length]; sourceIndexByMediaPeriod = new HashMap<>(); @@ -139,7 +156,7 @@ public final class ConcatenatingMediaSource implements MediaSource { return; } } - timeline = new ConcatenatedTimeline(timelines.clone(), isRepeatOneAtomic); + timeline = new ConcatenatedTimeline(timelines.clone(), isAtomic, shuffleOrder); listener.onSourceInfoRefreshed(timeline, manifests.clone()); } @@ -165,10 +182,10 @@ public final class ConcatenatingMediaSource implements MediaSource { private final Timeline[] timelines; private final int[] sourcePeriodOffsets; private final int[] sourceWindowOffsets; - private final boolean isRepeatOneAtomic; + private final boolean isAtomic; - public ConcatenatedTimeline(Timeline[] timelines, boolean isRepeatOneAtomic) { - super(new UnshuffledShuffleOrder(timelines.length)); + public ConcatenatedTimeline(Timeline[] timelines, boolean isAtomic, ShuffleOrder shuffleOrder) { + super(shuffleOrder); int[] sourcePeriodOffsets = new int[timelines.length]; int[] sourceWindowOffsets = new int[timelines.length]; long periodCount = 0; @@ -185,7 +202,7 @@ public final class ConcatenatingMediaSource implements MediaSource { this.timelines = timelines; this.sourcePeriodOffsets = sourcePeriodOffsets; this.sourceWindowOffsets = sourceWindowOffsets; - this.isRepeatOneAtomic = isRepeatOneAtomic; + this.isAtomic = isAtomic; } @Override @@ -201,19 +218,29 @@ public final class ConcatenatingMediaSource implements MediaSource { @Override public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { - if (isRepeatOneAtomic && repeatMode == Player.REPEAT_MODE_ONE) { + if (isAtomic && repeatMode == Player.REPEAT_MODE_ONE) { repeatMode = Player.REPEAT_MODE_ALL; } - return super.getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); + return super.getNextWindowIndex(windowIndex, repeatMode, !isAtomic && shuffleModeEnabled); } @Override public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { - if (isRepeatOneAtomic && repeatMode == Player.REPEAT_MODE_ONE) { + if (isAtomic && repeatMode == Player.REPEAT_MODE_ONE) { repeatMode = Player.REPEAT_MODE_ALL; } - return super.getPreviousWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); + return super.getPreviousWindowIndex(windowIndex, repeatMode, !isAtomic && shuffleModeEnabled); + } + + @Override + public int getLastWindowIndex(boolean shuffleModeEnabled) { + return super.getLastWindowIndex(!isAtomic && shuffleModeEnabled); + } + + @Override + public int getFirstWindowIndex(boolean shuffleModeEnabled) { + return super.getFirstWindowIndex(!isAtomic && shuffleModeEnabled); } @Override @@ -257,3 +284,4 @@ public final class ConcatenatingMediaSource implements MediaSource { } } + From f7eba77ee0378a0886ad9cb9d2b9ddeda8ed2285 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 07:30:29 -0700 Subject: [PATCH 0012/1327] Add shuffle support to dynamic concatenating media source. The media source is initialized with a DefaultShuffleOrder which can be changed at any time. Whenever the list of media source is changed, the shuffle order is adapted accordingly (either on the app thread if the player is not prepared yet, or on the player thread). The shuffle order is then used to construct the timeline. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166198488 --- .../DynamicConcatenatingMediaSourceTest.java | 27 ++++++++++++++-- .../DynamicConcatenatingMediaSource.java | 32 ++++++++++++++++--- 2 files changed, 52 insertions(+), 7 deletions(-) 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 9cdb461d7b..0e07e99978 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 @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.Player; 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.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.TimelineAsserts; @@ -49,7 +50,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { public void testPlaylistChangesAfterPreparation() throws InterruptedException { timeline = null; FakeMediaSource[] childSources = createMediaSources(7); - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( + new FakeShuffleOrder(0)); prepareAndListenToTimelineUpdates(mediaSource); waitForTimelineUpdate(); TimelineAsserts.assertEmpty(timeline); @@ -128,6 +130,18 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { C.INDEX_UNSET, 0, 1); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + assertEquals(0, timeline.getFirstWindowIndex(false)); + assertEquals(timeline.getWindowCount() - 1, timeline.getLastWindowIndex(false)); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); + assertEquals(timeline.getWindowCount() - 1, timeline.getFirstWindowIndex(true)); + assertEquals(0, timeline.getLastWindowIndex(true)); // Remove at front of queue. mediaSource.removeMediaSource(0); @@ -153,7 +167,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { public void testPlaylistChangesBeforePreparation() throws InterruptedException { timeline = null; FakeMediaSource[] childSources = createMediaSources(4); - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( + new FakeShuffleOrder(0)); mediaSource.addMediaSource(childSources[0]); mediaSource.addMediaSource(childSources[1]); mediaSource.addMediaSource(0, childSources[2]); @@ -168,6 +183,14 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertNotNull(timeline); TimelineAsserts.assertPeriodCounts(timeline, 3, 4, 2); TimelineAsserts.assertWindowIds(timeline, 333, 444, 222); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + 1, 2, C.INDEX_UNSET); mediaSource.releaseSource(); for (int i = 1; i < 4; i++) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 02d4bad2bf..3d0df7dcb3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -23,7 +23,7 @@ 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.source.ShuffleOrder.UnshuffledShuffleOrder; +import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; @@ -58,11 +58,26 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl private ExoPlayer player; private Listener listener; + private ShuffleOrder shuffleOrder; private boolean preventListenerNotification; private int windowCount; private int periodCount; + /** + * Creates a new dynamic concatenating media source. + */ public DynamicConcatenatingMediaSource() { + this(new DefaultShuffleOrder(0)); + } + + /** + * Creates a new dynamic concatenating media source with a custom shuffle order. + * + * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. + * This shuffle order must be empty. + */ + public DynamicConcatenatingMediaSource(ShuffleOrder shuffleOrder) { + this.shuffleOrder = shuffleOrder; this.mediaSourceByMediaPeriod = new IdentityHashMap<>(); this.mediaSourcesPublic = new ArrayList<>(); this.mediaSourceHolders = new ArrayList<>(); @@ -180,6 +195,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl this.player = player; this.listener = listener; preventListenerNotification = true; + shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size()); addMediaSourcesInternal(0, mediaSourcesPublic); preventListenerNotification = false; maybeNotifyListener(); @@ -234,21 +250,26 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl switch (messageType) { case MSG_ADD: { Pair messageData = (Pair) message; + shuffleOrder = shuffleOrder.cloneAndInsert(messageData.first, 1); addMediaSourceInternal(messageData.first, messageData.second); break; } case MSG_ADD_MULTIPLE: { Pair> messageData = (Pair>) message; + shuffleOrder = shuffleOrder.cloneAndInsert(messageData.first, messageData.second.size()); addMediaSourcesInternal(messageData.first, messageData.second); break; } case MSG_REMOVE: { + shuffleOrder = shuffleOrder.cloneAndRemove((Integer) message); removeMediaSourceInternal((Integer) message); break; } case MSG_MOVE: { Pair messageData = (Pair) message; + shuffleOrder = shuffleOrder.cloneAndRemove(messageData.first); + shuffleOrder = shuffleOrder.cloneAndInsert(messageData.second, 1); moveMediaSourceInternal(messageData.first, messageData.second); break; } @@ -262,8 +283,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl private void maybeNotifyListener() { if (!preventListenerNotification) { - listener.onSourceInfoRefreshed( - new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount), null); + listener.onSourceInfoRefreshed(new ConcatenatedTimeline(mediaSourceHolders, windowCount, + periodCount, shuffleOrder), null); } } @@ -397,8 +418,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl private final SparseIntArray childIndexByUid; public ConcatenatedTimeline(Collection mediaSourceHolders, int windowCount, - int periodCount) { - super(new UnshuffledShuffleOrder(mediaSourceHolders.size())); + int periodCount, ShuffleOrder shuffleOrder) { + super(shuffleOrder); this.windowCount = windowCount; this.periodCount = periodCount; int childCount = mediaSourceHolders.size(); @@ -638,3 +659,4 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } } + From 1305b1155b73cc106c616bebd59ab8592b47acc2 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 23 Aug 2017 07:32:53 -0700 Subject: [PATCH 0013/1327] Migrate MediaSessionConnector to API 26 for shuffle mode. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166198698 --- .../ext/mediasession/MediaSessionConnector.java | 10 +++++----- .../ext/mediasession/TimelineQueueNavigator.java | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 4dc1100c1e..3a4a80733d 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -232,9 +232,9 @@ public final class MediaSessionConnector { */ void onSkipToNext(Player player); /** - * See {@link MediaSessionCompat.Callback#onSetShuffleModeEnabled(boolean)}. + * See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}. */ - void onSetShuffleModeEnabled(Player player, boolean enabled); + void onSetShuffleMode(Player player, int shuffleMode); } /** @@ -803,15 +803,15 @@ public final class MediaSessionConnector { @Override public void onSetShuffleModeEnabled(boolean enabled) { if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) { - queueNavigator.onSetShuffleModeEnabled(player, enabled); + queueNavigator.onSetShuffleMode(player, enabled + ? PlaybackStateCompat.SHUFFLE_MODE_ALL : PlaybackStateCompat.SHUFFLE_MODE_NONE); } } @Override public void onSetShuffleMode(int shuffleMode) { if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) { - queueNavigator.onSetShuffleModeEnabled(player, - shuffleMode != PlaybackStateCompat.SHUFFLE_MODE_NONE); + queueNavigator.onSetShuffleMode(player, shuffleMode); } } diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 9d7ed75c83..8c7d3be114 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -160,8 +160,8 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu } @Override - public void onSetShuffleModeEnabled(Player player, boolean enabled) { - player.setShuffleModeEnabled(enabled); + public void onSetShuffleMode(Player player, int shuffleMode) { + player.setShuffleModeEnabled(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL); } private void publishFloatingQueueWindow(Player player) { From eeebb3968b2491539aaacd310d9ebc276faddfb7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 07:40:05 -0700 Subject: [PATCH 0014/1327] Implement shuffle mode logic in ExoPlayerImplInternal. This is mostly connecting the already stored shuffleMode with the timeline queries for the playback order. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166199330 --- .../android/exoplayer2/ExoPlayerTest.java | 25 +++++++++++++++ .../exoplayer2/ExoPlayerImplInternal.java | 31 ++++++++++++------- .../exoplayer2/MediaPeriodInfoSequence.java | 4 +-- .../android/exoplayer2/testutil/Action.java | 23 ++++++++++++++ .../exoplayer2/testutil/ActionSchedule.java | 10 ++++++ 5 files changed, 79 insertions(+), 14 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 bc72ebc060..9f172dc802 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,6 +15,7 @@ */ package com.google.android.exoplayer2; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -24,6 +25,7 @@ import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; 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.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import java.util.concurrent.CountDownLatch; @@ -234,4 +236,27 @@ public final class ExoPlayerTest extends TestCase { assertTrue(renderer.isEnded); } + public void testShuffleModeEnabledChanges() throws Exception { + Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000)); + MediaSource[] fakeMediaSources = { + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT) + }; + ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(false, + new FakeShuffleOrder(3), fakeMediaSources); + FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testShuffleModeEnabled") + .setRepeatMode(Player.REPEAT_MODE_ALL).waitForPositionDiscontinuity() // 0 -> 1 + .setShuffleModeEnabled(true).waitForPositionDiscontinuity() // 1 -> 0 + .waitForPositionDiscontinuity().waitForPositionDiscontinuity() // 0 -> 2 -> 1 + .setShuffleModeEnabled(false).setRepeatMode(Player.REPEAT_MODE_OFF) // 1 -> 2 -> end + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource).setRenderers(renderer).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPlayedPeriodIndices(0, 1, 0, 2, 1, 2); + assertTrue(renderer.isEnded); + } + } 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 7035ed637e..5d55652f61 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,7 +488,7 @@ import java.io.IOException; } while (true) { int nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.info.id.periodIndex, - period, window, repeatMode, false); + period, window, repeatMode, shuffleModeEnabled); while (lastValidPeriodHolder.next != null && !lastValidPeriodHolder.info.isLastInTimelinePeriod) { lastValidPeriodHolder = lastValidPeriodHolder.next; @@ -686,13 +686,15 @@ import java.io.IOException; Pair periodPosition = resolveSeekPosition(seekPosition); if (periodPosition == null) { + int firstPeriodIndex = timeline.isEmpty() ? 0 : timeline.getWindow( + timeline.getFirstWindowIndex(shuffleModeEnabled), window).firstPeriodIndex; // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. - playbackInfo = new PlaybackInfo(0, 0); + playbackInfo = new PlaybackInfo(firstPeriodIndex, 0); eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, playbackInfo).sendToTarget(); - // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't - // ignored. - playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); + // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to + // (firstPeriodIndex,0) isn't ignored. + playbackInfo = new PlaybackInfo(firstPeriodIndex, C.TIME_UNSET); setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false); @@ -1029,7 +1031,8 @@ import java.io.IOException; if (timeline.isEmpty()) { handleSourceInfoRefreshEndedPlayback(manifest); } else { - Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); + Pair defaultPosition = getPeriodPosition( + timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); int periodIndex = defaultPosition.first; long startPositionUs = defaultPosition.second; MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, @@ -1122,7 +1125,8 @@ import java.io.IOException; while (periodHolder.next != null) { MediaPeriodHolder previousPeriodHolder = periodHolder; periodHolder = periodHolder.next; - periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode, false); + periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode, + shuffleModeEnabled); 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. @@ -1170,11 +1174,14 @@ import java.io.IOException; private void handleSourceInfoRefreshEndedPlayback(Object manifest, int processedInitialSeekCount) { - // Set the playback position to (0,0) for notifying the eventHandler. - playbackInfo = new PlaybackInfo(0, 0); + int firstPeriodIndex = timeline.isEmpty() ? 0 : timeline.getWindow( + timeline.getFirstWindowIndex(shuffleModeEnabled), window).firstPeriodIndex; + // Set the playback position to (firstPeriodIndex,0) for notifying the eventHandler. + playbackInfo = new PlaybackInfo(firstPeriodIndex, 0); notifySourceInfoRefresh(manifest, processedInitialSeekCount); - // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't ignored. - playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); + // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to + // (firstPeriodIndex,0) isn't ignored. + playbackInfo = new PlaybackInfo(firstPeriodIndex, C.TIME_UNSET); setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false); @@ -1205,7 +1212,7 @@ 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, - false); + shuffleModeEnabled); if (oldPeriodIndex == C.INDEX_UNSET) { // We've reached the end of the old timeline. break; 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 index d7821ed705..6fd0d48e57 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java @@ -162,7 +162,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; // timeline is updated, to avoid repeatedly checking the same timeline. if (currentMediaPeriodInfo.isLastInTimelinePeriod) { int nextPeriodIndex = timeline.getNextPeriodIndex(currentMediaPeriodInfo.id.periodIndex, - period, window, repeatMode, false); + period, window, repeatMode, shuffleModeEnabled); if (nextPeriodIndex == C.INDEX_UNSET) { // We can't create a next period yet. return null; @@ -353,7 +353,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; 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, false) + && timeline.isLastPeriod(id.periodIndex, period, window, repeatMode, shuffleModeEnabled) && isLastMediaPeriodInPeriod; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index ab1f448afd..bc16e105da 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -284,6 +284,29 @@ public abstract class Action { } + /** + * Calls {@link Player#setShuffleModeEnabled(boolean)}. + */ + public static final class SetShuffleModeEnabled extends Action { + + private final boolean shuffleModeEnabled; + + /** + * @param tag A tag to use for logging. + */ + public SetShuffleModeEnabled(String tag, boolean shuffleModeEnabled) { + super(tag, "SetShuffleModeEnabled:" + shuffleModeEnabled); + this.shuffleModeEnabled = shuffleModeEnabled; + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + player.setShuffleModeEnabled(shuffleModeEnabled); + } + + } + /** * Waits for {@link Player.EventListener#onTimelineChanged(Timeline, Object)}. */ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 4392dd9d3f..c9ae02c957 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -30,6 +30,7 @@ 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.SetRepeatMode; +import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled; import com.google.android.exoplayer2.testutil.Action.SetVideoSurface; import com.google.android.exoplayer2.testutil.Action.Stop; import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity; @@ -217,6 +218,15 @@ public final class ActionSchedule { return apply(new SetRepeatMode(tag, repeatMode)); } + /** + * Schedules a shuffle setting action to be executed. + * + * @return The builder, for convenience. + */ + public Builder setShuffleModeEnabled(boolean shuffleModeEnabled) { + return apply(new SetShuffleModeEnabled(tag, shuffleModeEnabled)); + } + /** * Schedules a delay until the timeline changed to a specified expected timeline. * From aa712114c66b5e92d73d0529e1cbdb7242e9cd50 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 09:04:29 -0700 Subject: [PATCH 0015/1327] Force stop hosted test after timeout. When hosted tests run into a timeout, the outer test method stops. However, the hosted test itself may continue running and needs to be forced-stopped to ensure it does not block any resources needed by subsequent test methods. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166208204 --- .../google/android/exoplayer2/testutil/HostActivity.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 66b992e652..54087c4461 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 @@ -137,6 +137,12 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba fail(message); } } else { + runOnUiThread(new Runnable() { + @Override + public void run() { + hostedTest.forceStop(); + } + }); String message = "Test timed out after " + timeoutMs + " ms."; Log.e(TAG, message); if (failOnTimeout) { From 57bad31e4c17883de178f7a743798c42b6c88dc4 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 24 Aug 2017 03:37:32 -0700 Subject: [PATCH 0016/1327] Update documentation with new demo app location Plus a few misc doc fixes / adjustments ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166323135 --- README.md | 4 ++-- .../com/google/android/exoplayer2/ExoPlayer.java | 6 ++++++ .../source/DynamicConcatenatingMediaSource.java | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c67fb09d73..aff473c488 100644 --- a/README.md +++ b/README.md @@ -71,12 +71,12 @@ individually. In addition to library modules, ExoPlayer has multiple extension modules that depend on external libraries to provide additional functionality. Some extensions are available from JCenter, whereas others must be built manaully. -Browse the [extensions directory] and their individual READMEs for details. +Browse the [extensions directory][] and their individual READMEs for details. More information on the library and extension modules that are available from JCenter can be found on [Bintray][]. -[extensions directory][]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ +[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ [Bintray]: https://bintray.com/google/exoplayer ### Locally ### 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 b096b5ae12..915a083657 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 @@ -211,12 +211,18 @@ public interface ExoPlayer extends Player { /** * Prepares the player to play the provided {@link MediaSource}. Equivalent to * {@code prepare(mediaSource, true, true)}. + *

+ * Note: {@link MediaSource} instances are not designed to be re-used. If you want to prepare a + * player more than once with the same piece of media, use a new instance each time. */ void prepare(MediaSource mediaSource); /** * Prepares the player to play the provided {@link MediaSource}, optionally resetting the playback * position the default position in the first {@link Timeline.Window}. + *

+ * Note: {@link MediaSource} instances are not designed to be re-used. If you want to prepare a + * player more than once with the same piece of media, use a new instance each time. * * @param mediaSource The {@link MediaSource} to play. * @param resetPosition Whether the playback position should be reset to the default position in diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 3d0df7dcb3..9c1e7ec1ba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -87,6 +87,9 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl /** * Appends a {@link MediaSource} to the playlist. + *

+ * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same + * piece of media multiple times, use a new instance each time. * * @param mediaSource The {@link MediaSource} to be added to the list. */ @@ -96,6 +99,9 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl /** * Adds a {@link MediaSource} to the playlist. + *

+ * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same + * piece of media multiple times, use a new instance each time. * * @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()}. @@ -112,6 +118,9 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl /** * Appends multiple {@link MediaSource}s to the playlist. + *

+ * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same + * piece of media multiple times, use a new instance each time. * * @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. @@ -122,6 +131,9 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl /** * Adds multiple {@link MediaSource}s to the playlist. + *

+ * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same + * piece of media multiple times, use a new instance each time. * * @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()}. @@ -142,6 +154,10 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl /** * Removes a {@link MediaSource} from the playlist. + *

+ * Note: {@link MediaSource} instances are not designed to be re-used, and so the instance being + * removed should not be re-added. If you want to move the instance use + * {@link #moveMediaSource(int, int)} instead. * * @param index The index at which the media source will be removed. This index must be in the * range of 0 <= index < {@link #getSize()}. From 6e03dcdfa1fa18a7865085186b7e33790489d46c Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 24 Aug 2017 05:30:36 -0700 Subject: [PATCH 0017/1327] Add LocalMediaDrmCallback. Useful for providing local keys. Issue: #3178 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166330215 --- .../exoplayer2/drm/LocalMediaDrmCallback.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java new file mode 100644 index 0000000000..7b9aeca30a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java @@ -0,0 +1,51 @@ +/* + * 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.drm; + +import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; +import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.UUID; + +/** + * A {@link MediaDrmCallback} that provides a fixed response to key requests. Provisioning is not + * supported. This implementation is primarily useful for providing locally stored keys to decrypt + * ClearKey protected content. It is not suitable for use with Widevine or PlayReady protected + * content. + */ +public final class LocalMediaDrmCallback implements MediaDrmCallback { + + private final byte[] keyResponse; + + /** + * @param keyResponse The fixed response for all key requests. + */ + public LocalMediaDrmCallback(byte[] keyResponse) { + this.keyResponse = Assertions.checkNotNull(keyResponse); + } + + @Override + public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { + return keyResponse; + } + +} From 6907ffb2852d56556c5a4186d6d7cfb00d8d7553 Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 23 Jul 2017 09:55:50 +0100 Subject: [PATCH 0018/1327] Remove unnecessary view casts findViewById is now defined using generics, which allows the types to be inferred. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166355555 --- .../android/exoplayer2/castdemo/MainActivity.java | 6 +++--- .../google/android/exoplayer2/demo/PlayerActivity.java | 8 ++++---- .../android/exoplayer2/demo/SampleChooserActivity.java | 2 +- .../android/exoplayer2/ui/PlaybackControlView.java | 8 ++++---- .../android/exoplayer2/ui/SimpleExoPlayerView.java | 10 +++++----- .../android/exoplayer2/testutil/HostActivity.java | 2 +- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index e1367858aa..e1c7519a05 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -49,12 +49,12 @@ public class MainActivity extends AppCompatActivity { setContentView(R.layout.main_activity); - simpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.player_view); + simpleExoPlayerView = findViewById(R.id.player_view); simpleExoPlayerView.requestFocus(); - castControlView = (PlaybackControlView) findViewById(R.id.cast_control_view); + castControlView = findViewById(R.id.cast_control_view); - ListView sampleList = (ListView) findViewById(R.id.sample_list); + ListView sampleList = findViewById(R.id.sample_list); sampleList.setAdapter(new SampleListAdapter()); sampleList.setOnItemClickListener(new SampleClickListener()); } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 6d733c9f97..b2750a93bb 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -148,12 +148,12 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi setContentView(R.layout.player_activity); View rootView = findViewById(R.id.root); rootView.setOnClickListener(this); - debugRootView = (LinearLayout) findViewById(R.id.controls_root); - debugTextView = (TextView) findViewById(R.id.debug_text_view); - retryButton = (Button) findViewById(R.id.retry_button); + debugRootView = findViewById(R.id.controls_root); + debugTextView = findViewById(R.id.debug_text_view); + retryButton = findViewById(R.id.retry_button); retryButton.setOnClickListener(this); - simpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.player_view); + simpleExoPlayerView = findViewById(R.id.player_view); simpleExoPlayerView.setControllerVisibilityListener(this); simpleExoPlayerView.requestFocus(); } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 382c783598..c0edb1d1b8 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -90,7 +90,7 @@ public class SampleChooserActivity extends Activity { Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) .show(); } - ExpandableListView sampleList = (ExpandableListView) findViewById(R.id.sample_list); + ExpandableListView sampleList = findViewById(R.id.sample_list); sampleList.setAdapter(new SampleAdapter(this, groups)); sampleList.setOnChildClickListener(new OnChildClickListener() { @Override 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 105dbc2495..91308848f9 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 @@ -374,9 +374,9 @@ public class PlaybackControlView extends FrameLayout { LayoutInflater.from(context).inflate(controllerLayoutId, this); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); - durationView = (TextView) findViewById(R.id.exo_duration); - positionView = (TextView) findViewById(R.id.exo_position); - timeBar = (TimeBar) findViewById(R.id.exo_progress); + durationView = findViewById(R.id.exo_duration); + positionView = findViewById(R.id.exo_position); + timeBar = findViewById(R.id.exo_progress); if (timeBar != null) { timeBar.setListener(componentListener); } @@ -404,7 +404,7 @@ public class PlaybackControlView extends FrameLayout { if (fastForwardButton != null) { fastForwardButton.setOnClickListener(componentListener); } - repeatToggleButton = (ImageView) findViewById(R.id.exo_repeat_toggle); + repeatToggleButton = findViewById(R.id.exo_repeat_toggle); if (repeatToggleButton != null) { repeatToggleButton.setOnClickListener(componentListener); } 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 bdbdf34331..0a531375c1 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 @@ -286,7 +286,7 @@ public final class SimpleExoPlayerView extends FrameLayout { setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); // Content frame. - contentFrame = (AspectRatioFrameLayout) findViewById(R.id.exo_content_frame); + contentFrame = findViewById(R.id.exo_content_frame); if (contentFrame != null) { setResizeModeRaw(contentFrame, resizeMode); } @@ -307,24 +307,24 @@ public final class SimpleExoPlayerView extends FrameLayout { } // Overlay frame layout. - overlayFrameLayout = (FrameLayout) findViewById(R.id.exo_overlay); + overlayFrameLayout = findViewById(R.id.exo_overlay); // Artwork view. - artworkView = (ImageView) findViewById(R.id.exo_artwork); + artworkView = findViewById(R.id.exo_artwork); this.useArtwork = useArtwork && artworkView != null; if (defaultArtworkId != 0) { defaultArtwork = BitmapFactory.decodeResource(context.getResources(), defaultArtworkId); } // Subtitle view. - subtitleView = (SubtitleView) findViewById(R.id.exo_subtitles); + subtitleView = findViewById(R.id.exo_subtitles); if (subtitleView != null) { subtitleView.setUserDefaultStyle(); subtitleView.setUserDefaultTextSize(); } // Playback control view. - PlaybackControlView customController = (PlaybackControlView) findViewById(R.id.exo_controller); + PlaybackControlView customController = findViewById(R.id.exo_controller); View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); if (customController != null) { this.controller = customController; 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 54087c4461..299cb10815 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 @@ -159,7 +159,7 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(getResources().getIdentifier("host_activity", "layout", getPackageName())); - surfaceView = (SurfaceView) findViewById( + surfaceView = findViewById( getResources().getIdentifier("surface_view", "id", getPackageName())); surfaceView.getHolder().addCallback(this); } From 1b9c904dbae7009fcb448b0db3b2286289f14925 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 25 Aug 2017 02:52:35 -0700 Subject: [PATCH 0019/1327] Add UI for shuffle mode. This includes an option to show and hide the shuffle mode button. When pressing the button, the shuffle mode of the player is toggled. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166455759 --- .../exoplayer2/ui/PlaybackControlView.java | 73 ++++++++++++++++++- .../exoplayer2/ui/SimpleExoPlayerView.java | 10 +++ .../res/layout/exo_playback_control_view.xml | 3 + library/ui/src/main/res/values/attrs.xml | 2 + 4 files changed, 87 insertions(+), 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 91308848f9..f83bab5770 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 @@ -84,6 +84,12 @@ import java.util.Locale; *

  • Default: {@link PlaybackControlView#DEFAULT_REPEAT_TOGGLE_MODES}
  • * * + *
  • {@code show_shuffle_button} - Whether the shuffle button is shown. + *
      + *
    • Corresponding method: {@link #setShowShuffleButton(boolean)}
    • + *
    • Default: false
    • + *
    + *
  • *
  • {@code controller_layout_id} - Specifies the id of the layout to be inflated. See * below for more details. *
      @@ -136,6 +142,11 @@ import java.util.Locale; *
    • Type: {@link View}
    • *
    *
  • + *
  • {@code exo_shuffle} - The shuffle button. + *
      + *
    • Type: {@link View}
    • + *
    + *
  • *
  • {@code exo_position} - Text view displaying the current playback position. *
      *
    • Type: {@link TextView}
    • @@ -221,6 +232,15 @@ public class PlaybackControlView extends FrameLayout { */ boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode); + /** + * Dispatches a {@link Player#setShuffleModeEnabled(boolean)} operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @param shuffleModeEnabled Whether shuffling is enabled. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled); + } /** @@ -247,6 +267,12 @@ public class PlaybackControlView extends FrameLayout { return true; } + @Override + public boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled) { + player.setShuffleModeEnabled(shuffleModeEnabled); + return true; + } + }; /** @@ -282,6 +308,7 @@ public class PlaybackControlView extends FrameLayout { private final View fastForwardButton; private final View rewindButton; private final ImageView repeatToggleButton; + private final View shuffleButton; private final TextView durationView; private final TextView positionView; private final TimeBar timeBar; @@ -309,6 +336,7 @@ public class PlaybackControlView extends FrameLayout { private int fastForwardMs; private int showTimeoutMs; private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes; + private boolean showShuffleButton; private long hideAtMs; private long[] adGroupTimesMs; private boolean[] playedAdGroups; @@ -345,6 +373,7 @@ public class PlaybackControlView extends FrameLayout { fastForwardMs = DEFAULT_FAST_FORWARD_MS; showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; + showShuffleButton = false; if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlaybackControlView, 0, 0); @@ -356,6 +385,8 @@ public class PlaybackControlView extends FrameLayout { controllerLayoutId = a.getResourceId(R.styleable.PlaybackControlView_controller_layout_id, controllerLayoutId); repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes); + showShuffleButton = a.getBoolean(R.styleable.PlaybackControlView_show_shuffle_button, + showShuffleButton); } finally { a.recycle(); } @@ -408,6 +439,10 @@ public class PlaybackControlView extends FrameLayout { if (repeatToggleButton != null) { repeatToggleButton.setOnClickListener(componentListener); } + shuffleButton = findViewById(R.id.exo_shuffle); + if (shuffleButton != null) { + shuffleButton.setOnClickListener(componentListener); + } Resources resources = context.getResources(); repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off); repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one); @@ -584,6 +619,23 @@ public class PlaybackControlView extends FrameLayout { } } + /** + * Returns whether the shuffle button is shown. + */ + public boolean getShowShuffleButton() { + return showShuffleButton; + } + + /** + * Sets whether the shuffle button is shown. + * + * @param showShuffleButton Whether the shuffle button is shown. + */ + public void setShowShuffleButton(boolean showShuffleButton) { + this.showShuffleButton = showShuffleButton; + updateShuffleButton(); + } + /** * 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. @@ -639,6 +691,7 @@ public class PlaybackControlView extends FrameLayout { updatePlayPauseButton(); updateNavigation(); updateRepeatModeButton(); + updateShuffleButton(); updateProgress(); } @@ -721,6 +774,21 @@ public class PlaybackControlView extends FrameLayout { repeatToggleButton.setVisibility(View.VISIBLE); } + private void updateShuffleButton() { + if (!isVisible() || !isAttachedToWindow || shuffleButton == null) { + return; + } + if (!showShuffleButton) { + shuffleButton.setVisibility(View.GONE); + } else if (player == null) { + setButtonEnabled(false, shuffleButton); + } else { + shuffleButton.setAlpha(player.getShuffleModeEnabled() ? 1f : 0.3f); + shuffleButton.setEnabled(true); + shuffleButton.setVisibility(View.VISIBLE); + } + } + private void updateTimeBarMode() { if (player == null) { return; @@ -1080,7 +1148,8 @@ public class PlaybackControlView extends FrameLayout { @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - // TODO: Update UI. + updateShuffleButton(); + updateNavigation(); } @Override @@ -1134,6 +1203,8 @@ public class PlaybackControlView extends FrameLayout { } else if (repeatToggleButton == view) { controlDispatcher.dispatchSetRepeatMode(player, RepeatModeUtil.getNextRepeatMode( player.getRepeatMode(), repeatToggleModes)); + } else if (shuffleButton == view) { + controlDispatcher.dispatchSetShuffleModeEnabled(player, !player.getShuffleModeEnabled()); } } 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 0a531375c1..a8926f9ecc 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 @@ -655,6 +655,16 @@ public final class SimpleExoPlayerView extends FrameLayout { controller.setRepeatToggleModes(repeatToggleModes); } + /** + * Sets whether the shuffle button is shown. + * + * @param showShuffleButton Whether the shuffle button is shown. + */ + public void setShowShuffleButton(boolean showShuffleButton) { + Assertions.checkState(controller != null); + controller.setShowShuffleButton(showShuffleButton); + } + /** * Sets whether the time bar should show all windows, as opposed to just the current one. * 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 407329890d..159844c234 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 d1f45228b1..9b701d5ba5 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -39,6 +39,7 @@ + @@ -64,6 +65,7 @@ + From 01f481984437ce94bed16e71131d0ba084a2b70c Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 25 Aug 2017 07:43:15 -0700 Subject: [PATCH 0020/1327] Introduce MediaSessionConnector.CommandReceiver interface and add TimelineQueueEditor. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166475351 --- .../DefaultPlaybackController.java | 12 + .../mediasession/MediaSessionConnector.java | 86 +++++-- .../ext/mediasession/TimelineQueueEditor.java | 226 ++++++++++++++++++ .../mediasession/TimelineQueueNavigator.java | 14 ++ 4 files changed, 316 insertions(+), 22 deletions(-) create mode 100644 extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java index c3586b29e6..e01d6a48db 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.ext.mediasession; +import android.os.Bundle; +import android.os.ResultReceiver; import android.support.v4.media.session.PlaybackStateCompat; import com.google.android.exoplayer2.C; @@ -125,4 +127,14 @@ public class DefaultPlaybackController implements MediaSessionConnector.Playback player.stop(); } + @Override + public String[] getCommands() { + return null; + } + + @Override + public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) { + // Do nothing. + } + } diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 3a4a80733d..a64f163733 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -79,10 +79,24 @@ public final class MediaSessionConnector { private static final int EDITOR_MEDIA_SESSION_FLAGS = BASE_MEDIA_SESSION_FLAGS | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; + /** + * Receiver of media commands sent by a media controller. + */ + public interface CommandReceiver { + /** + * Returns the commands the receiver handles, or {@code null} if no commands need to be handled. + */ + String[] getCommands(); + /** + * See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. + */ + void onCommand(Player player, String command, Bundle extras, ResultReceiver cb); + } + /** * Interface to which playback preparation actions are delegated. */ - public interface PlaybackPreparer { + public interface PlaybackPreparer extends CommandReceiver { long ACTIONS = PlaybackStateCompat.ACTION_PREPARE | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID @@ -121,16 +135,12 @@ public final class MediaSessionConnector { * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. */ void onPrepareFromUri(Uri uri, Bundle extras); - /** - * See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. - */ - void onCommand(String command, Bundle extras, ResultReceiver cb); } /** * Interface to which playback actions are delegated. */ - public interface PlaybackController { + public interface PlaybackController extends CommandReceiver { long ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SEEK_TO @@ -178,7 +188,7 @@ public final class MediaSessionConnector { * Handles queue navigation actions, and updates the media session queue by calling * {@code MediaSessionCompat.setQueue()}. */ - public interface QueueNavigator { + public interface QueueNavigator extends CommandReceiver { long ACTIONS = PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS @@ -240,7 +250,7 @@ public final class MediaSessionConnector { /** * Handles media session queue edits. */ - public interface QueueEditor { + public interface QueueEditor extends CommandReceiver { long ACTIONS = PlaybackStateCompat.ACTION_SET_RATING; @@ -309,6 +319,7 @@ public final class MediaSessionConnector { private final ExoPlayerEventListener exoPlayerEventListener; private final MediaSessionCallback mediaSessionCallback; private final PlaybackController playbackController; + private final Map commandMap; private Player player; private CustomActionProvider[] customActionProviders; @@ -328,7 +339,7 @@ public final class MediaSessionConnector { * @param mediaSession The {@link MediaSessionCompat} to connect to. */ public MediaSessionConnector(MediaSessionCompat mediaSession) { - this(mediaSession, new DefaultPlaybackController()); + this(mediaSession, null); } /** @@ -350,7 +361,8 @@ public final class MediaSessionConnector { * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. - * @param playbackController A {@link PlaybackController} for handling playback actions. + * @param playbackController A {@link PlaybackController} for handling playback actions, or + * {@code null} if the connector should handle playback actions directly. * @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If * {@code false}, you need to maintain the metadata of the media session yourself (provide at * least the duration to allow clients to show a progress bar). @@ -358,7 +370,8 @@ public final class MediaSessionConnector { public MediaSessionConnector(MediaSessionCompat mediaSession, PlaybackController playbackController, boolean doMaintainMetadata) { this.mediaSession = mediaSession; - this.playbackController = playbackController; + this.playbackController = playbackController != null ? playbackController + : new DefaultPlaybackController(); this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); this.doMaintainMetadata = doMaintainMetadata; @@ -367,6 +380,8 @@ public final class MediaSessionConnector { mediaSessionCallback = new MediaSessionCallback(); exoPlayerEventListener = new ExoPlayerEventListener(); customActionMap = Collections.emptyMap(); + commandMap = new HashMap<>(); + registerCommandReceiver(playbackController); } /** @@ -386,8 +401,12 @@ public final class MediaSessionConnector { this.player.removeListener(exoPlayerEventListener); mediaSession.setCallback(null); } - this.playbackPreparer = playbackPreparer; + unregisterCommandReceiver(this.playbackPreparer); + this.player = player; + this.playbackPreparer = playbackPreparer; + registerCommandReceiver(playbackPreparer); + this.customActionProviders = (player != null && customActionProviders != null) ? customActionProviders : new CustomActionProvider[0]; if (player != null) { @@ -416,7 +435,9 @@ public final class MediaSessionConnector { * @param queueNavigator The queue navigator. */ public void setQueueNavigator(QueueNavigator queueNavigator) { + unregisterCommandReceiver(this.queueNavigator); this.queueNavigator = queueNavigator; + registerCommandReceiver(queueNavigator); } /** @@ -425,11 +446,29 @@ public final class MediaSessionConnector { * @param queueEditor The queue editor. */ public void setQueueEditor(QueueEditor queueEditor) { + unregisterCommandReceiver(this.queueEditor); this.queueEditor = queueEditor; + registerCommandReceiver(queueEditor); mediaSession.setFlags(queueEditor == null ? BASE_MEDIA_SESSION_FLAGS : EDITOR_MEDIA_SESSION_FLAGS); } + private void registerCommandReceiver(CommandReceiver commandReceiver) { + if (commandReceiver != null && commandReceiver.getCommands() != null) { + for (String command : commandReceiver.getCommands()) { + commandMap.put(command, commandReceiver); + } + } + } + + private void unregisterCommandReceiver(CommandReceiver commandReceiver) { + if (commandReceiver != null && commandReceiver.getCommands() != null) { + for (String command : commandReceiver.getCommands()) { + commandMap.remove(command); + } + } + } + private void updateMediaSessionPlaybackState() { PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); if (player == null) { @@ -473,11 +512,8 @@ public final class MediaSessionConnector { } private long buildPlaybackActions() { - long actions = 0; - if (playbackController != null) { - actions |= (PlaybackController.ACTIONS & playbackController - .getSupportedPlaybackActions(player)); - } + long actions = (PlaybackController.ACTIONS + & playbackController.getSupportedPlaybackActions(player)); if (playbackPreparer != null) { actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions()); } @@ -562,7 +598,7 @@ public final class MediaSessionConnector { } private boolean canDispatchToPlaybackController(long action) { - return playbackController != null && (playbackController.getSupportedPlaybackActions(player) + return (playbackController.getSupportedPlaybackActions(player) & PlaybackController.ACTIONS & action) != 0; } @@ -583,10 +619,15 @@ public final class MediaSessionConnector { @Override public void onTimelineChanged(Timeline timeline, Object manifest) { + int windowCount = player.getCurrentTimeline().getWindowCount(); + int windowIndex = player.getCurrentWindowIndex(); if (queueNavigator != null) { queueNavigator.onTimelineChanged(player); + updateMediaSessionPlaybackState(); + } else if (currentWindowCount != windowCount || currentWindowIndex != windowIndex) { + // active queue item and queue navigation actions may need to be updated + updateMediaSessionPlaybackState(); } - int windowCount = player.getCurrentTimeline().getWindowCount(); if (currentWindowCount != windowCount) { // active queue item and queue navigation actions may need to be updated updateMediaSessionPlaybackState(); @@ -638,8 +679,8 @@ public final class MediaSessionConnector { if (queueNavigator != null) { queueNavigator.onCurrentWindowIndexChanged(player); } - updateMediaSessionMetadata(); currentWindowIndex = player.getCurrentWindowIndex(); + updateMediaSessionMetadata(); } updateMediaSessionPlaybackState(); } @@ -732,8 +773,9 @@ public final class MediaSessionConnector { @Override public void onCommand(String command, Bundle extras, ResultReceiver cb) { - if (playbackPreparer != null) { - playbackPreparer.onCommand(command, extras, cb); + CommandReceiver commandReceiver = commandMap.get(command); + if (commandReceiver != null) { + commandReceiver.onCommand(player, command, extras, cb); } } diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java new file mode 100644 index 0000000000..65090a3c1c --- /dev/null +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java @@ -0,0 +1,226 @@ +/* + * 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.mediasession; + +import android.os.Bundle; +import android.os.ResultReceiver; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.util.Util; +import java.util.List; + +/** + * A {@link MediaSessionConnector.QueueEditor} implementation based on the + * {@link DynamicConcatenatingMediaSource}. + *

      + * This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles + * the {@link #COMMAND_MOVE_QUEUE_ITEM} to move a queue item instead of removing and inserting it. + * This allows to move the currently playing window without interrupting playback. + */ +public final class TimelineQueueEditor implements MediaSessionConnector.QueueEditor, + MediaSessionConnector.CommandReceiver { + + public static final String COMMAND_MOVE_QUEUE_ITEM = "exo_move_window"; + public static final String EXTRA_FROM_INDEX = "from_index"; + public static final String EXTRA_TO_INDEX = "to_index"; + + /** + * Factory to create {@link MediaSource}s. + */ + public interface MediaSourceFactory { + /** + * Creates a {@link MediaSource} for the given {@link MediaDescriptionCompat}. + * + * @param description The {@link MediaDescriptionCompat} to create a media source for. + * @return A {@link MediaSource} or {@code null} if no source can be created for the given + * description. + */ + @Nullable MediaSource createMediaSource(MediaDescriptionCompat description); + } + + /** + * Adapter to get {@link MediaDescriptionCompat} of items in the queue and to notify the + * application about changes in the queue to sync the data structure backing the + * {@link MediaSessionConnector}. + */ + public interface QueueDataAdapter { + /** + * Gets the {@link MediaDescriptionCompat} for a {@code position}. + * + * @param position The position in the queue for which to provide a description. + * @return A {@link MediaDescriptionCompat}. + */ + MediaDescriptionCompat getMediaDescription(int position); + /** + * Adds a {@link MediaDescriptionCompat} at the given {@code position}. + * + * @param position The position at which to add. + * @param description The {@link MediaDescriptionCompat} to be added. + */ + void add(int position, MediaDescriptionCompat description); + /** + * Removes the item at the given {@code position}. + * + * @param position The position at which to remove the item. + */ + void remove(int position); + /** + * Moves a queue item from position {@code from} to position {@code to}. + * + * @param from The position from which to remove the item. + * @param to The target position to which to move the item. + */ + void move(int from, int to); + } + + /** + * Used to evaluate whether two {@link MediaDescriptionCompat} are considered equal. + */ + interface MediaDescriptionEqualityChecker { + /** + * Returns {@code true} whether the descriptions are considered equal. + * + * @param d1 The first {@link MediaDescriptionCompat}. + * @param d2 The second {@link MediaDescriptionCompat}. + * @return {@code true} if considered equal. + */ + boolean equals(MediaDescriptionCompat d1, MediaDescriptionCompat d2); + } + + /** + * Media description comparator comparing the media IDs. Media IDs are considered equals if both + * are {@code null}. + */ + public static final class MediaIdEqualityChecker implements MediaDescriptionEqualityChecker { + + @Override + public boolean equals(MediaDescriptionCompat d1, MediaDescriptionCompat d2) { + return Util.areEqual(d1.getMediaId(), d2.getMediaId()); + } + + } + + private final MediaControllerCompat mediaController; + private final QueueDataAdapter queueDataAdapter; + private final MediaSourceFactory sourceFactory; + private final MediaDescriptionEqualityChecker equalityChecker; + private final DynamicConcatenatingMediaSource queueMediaSource; + + /** + * Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory. + * + * @param mediaController A {@link MediaControllerCompat} to read the current queue. + * @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to + * manipulate. + * @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data. + * @param sourceFactory The {@link MediaSourceFactory} to build media sources. + */ + public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController, + @NonNull DynamicConcatenatingMediaSource queueMediaSource, + @NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory) { + this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory, + new MediaIdEqualityChecker()); + } + + /** + * Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory. + * + * @param mediaController A {@link MediaControllerCompat} to read the current queue. + * @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to + * manipulate. + * @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data. + * @param sourceFactory The {@link MediaSourceFactory} to build media sources. + * @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items. + */ + public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController, + @NonNull DynamicConcatenatingMediaSource queueMediaSource, + @NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory, + @NonNull MediaDescriptionEqualityChecker equalityChecker) { + this.mediaController = mediaController; + this.queueMediaSource = queueMediaSource; + this.queueDataAdapter = queueDataAdapter; + this.sourceFactory = sourceFactory; + this.equalityChecker = equalityChecker; + } + + @Override + public long getSupportedQueueEditorActions(@Nullable Player player) { + return 0; + } + + @Override + public void onAddQueueItem(Player player, MediaDescriptionCompat description) { + onAddQueueItem(player, description, player.getCurrentTimeline().getWindowCount()); + } + + @Override + public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) { + MediaSource mediaSource = sourceFactory.createMediaSource(description); + if (mediaSource != null) { + queueDataAdapter.add(index, description); + queueMediaSource.addMediaSource(index, mediaSource); + } + } + + @Override + public void onRemoveQueueItem(Player player, MediaDescriptionCompat description) { + List queue = mediaController.getQueue(); + for (int i = 0; i < queue.size(); i++) { + if (equalityChecker.equals(queue.get(i).getDescription(), description)) { + onRemoveQueueItemAt(player, i); + return; + } + } + } + + @Override + public void onRemoveQueueItemAt(Player player, int index) { + queueDataAdapter.remove(index); + queueMediaSource.removeMediaSource(index); + } + + @Override + public void onSetRating(Player player, RatingCompat rating) { + // Do nothing. + } + + // CommandReceiver implementation. + + @NonNull + @Override + public String[] getCommands() { + return new String[] {COMMAND_MOVE_QUEUE_ITEM}; + } + + @Override + public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) { + int from = extras.getInt(EXTRA_FROM_INDEX, C.INDEX_UNSET); + int to = extras.getInt(EXTRA_TO_INDEX, C.INDEX_UNSET); + if (from != C.INDEX_UNSET && to != C.INDEX_UNSET) { + queueDataAdapter.move(from, to); + queueMediaSource.moveMediaSource(from, to); + } + } + +} diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 8c7d3be114..777949863d 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.ext.mediasession; +import android.os.Bundle; +import android.os.ResultReceiver; import android.support.annotation.Nullable; import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.session.MediaSessionCompat; @@ -164,6 +166,18 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu player.setShuffleModeEnabled(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL); } + // CommandReceiver implementation. + + @Override + public String[] getCommands() { + return null; + } + + @Override + public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) { + // Do nothing. + } + private void publishFloatingQueueWindow(Player player) { if (player.getCurrentTimeline().isEmpty()) { mediaSession.setQueue(Collections.emptyList()); From 30b31b56792664ebe181bc497282f075f6add1e5 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 25 Aug 2017 08:37:38 -0700 Subject: [PATCH 0021/1327] Support empty concatenations and empty timelines in concatenations. Both cases were not supported so far. Added tests which all failed in the previous code version and adapted the concatenated media sources to cope with empty timelines and empty concatenations. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166480344 --- .../source/ConcatenatingMediaSourceTest.java | 67 ++++++++++- .../DynamicConcatenatingMediaSourceTest.java | 110 +++++++++++++++++- .../source/LoopingMediaSourceTest.java | 17 ++- .../google/android/exoplayer2/Timeline.java | 10 +- .../source/AbstractConcatenatedTimeline.java | 55 ++++++++- .../source/ConcatenatingMediaSource.java | 26 +++-- .../DynamicConcatenatingMediaSource.java | 13 ++- .../exoplayer2/source/LoopingMediaSource.java | 6 +- .../exoplayer2/testutil/TimelineAsserts.java | 7 +- 9 files changed, 275 insertions(+), 36 deletions(-) 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 fd0acf2ab3..53111e83ac 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 @@ -31,11 +31,24 @@ import junit.framework.TestCase; */ public final class ConcatenatingMediaSourceTest extends TestCase { + public void testEmptyConcatenation() { + for (boolean atomic : new boolean[] {false, true}) { + Timeline timeline = getConcatenatedTimeline(atomic); + TimelineAsserts.assertEmpty(timeline); + + timeline = getConcatenatedTimeline(atomic, Timeline.EMPTY); + TimelineAsserts.assertEmpty(timeline); + + timeline = getConcatenatedTimeline(atomic, Timeline.EMPTY, Timeline.EMPTY, Timeline.EMPTY); + TimelineAsserts.assertEmpty(timeline); + } + } + public void testSingleMediaSource() { Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); @@ -49,7 +62,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); @@ -91,7 +104,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { timeline = getConcatenatedTimeline(true, timelines); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, @@ -135,6 +148,54 @@ public final class ConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 3, 1); } + public void testEmptyTimelineMediaSources() { + // Empty timelines in the front, back, and the middle (single and multiple in a row). + Timeline[] timelines = { Timeline.EMPTY, createFakeTimeline(1, 111), Timeline.EMPTY, + Timeline.EMPTY, createFakeTimeline(2, 222), Timeline.EMPTY, createFakeTimeline(3, 333), + Timeline.EMPTY }; + Timeline timeline = getConcatenatedTimeline(false, timelines); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); + assertEquals(0, timeline.getFirstWindowIndex(false)); + assertEquals(2, timeline.getLastWindowIndex(false)); + assertEquals(2, timeline.getFirstWindowIndex(true)); + assertEquals(0, timeline.getLastWindowIndex(true)); + + timeline = getConcatenatedTimeline(true, timelines); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); + for (boolean shuffled : new boolean[] {false, true}) { + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, + 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, + 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); + assertEquals(0, timeline.getFirstWindowIndex(shuffled)); + assertEquals(2, timeline.getLastWindowIndex(shuffled)); + } + } + /** * Wraps the specified timelines in a {@link ConcatenatingMediaSource} and returns * the concatenated timeline. 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 0e07e99978..86c03d1ce8 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -25,7 +26,10 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaPeriod.Callback; import com.google.android.exoplayer2.source.MediaSource.Listener; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.testutil.FakeMediaPeriod; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; @@ -53,13 +57,13 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( new FakeShuffleOrder(0)); prepareAndListenToTimelineUpdates(mediaSource); + assertNotNull(timeline); waitForTimelineUpdate(); TimelineAsserts.assertEmpty(timeline); // Add first source. mediaSource.addMediaSource(childSources[0]); waitForTimelineUpdate(); - assertNotNull(timeline); TimelineAsserts.assertPeriodCounts(timeline, 1); TimelineAsserts.assertWindowIds(timeline, 111); @@ -143,6 +147,9 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertEquals(timeline.getWindowCount() - 1, timeline.getFirstWindowIndex(true)); assertEquals(0, timeline.getLastWindowIndex(true)); + // Assert all periods can be prepared. + assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); + // Remove at front of queue. mediaSource.removeMediaSource(0); waitForTimelineUpdate(); @@ -192,6 +199,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); + assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); mediaSource.releaseSource(); for (int i = 1; i < 4; i++) { childSources[i].assertReleased(); @@ -225,8 +233,9 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertPeriodCounts(timeline, 1, 9); TimelineAsserts.assertWindowIds(timeline, 111, 999); TimelineAsserts.assertWindowIsDynamic(timeline, false, false); + assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); - //Add lazy sources after preparation + //Add lazy sources after preparation (and also try to prepare media period from lazy source). mediaSource.addMediaSource(1, lazySources[2]); waitForTimelineUpdate(); mediaSource.addMediaSource(2, childSources[1]); @@ -239,17 +248,90 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999); TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false); + MediaPeriod lazyPeriod = mediaSource.createPeriod(new MediaPeriodId(0), null); + assertNotNull(lazyPeriod); + final ConditionVariable lazyPeriodPrepared = new ConditionVariable(); + lazyPeriod.prepare(new Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + lazyPeriodPrepared.open(); + } + @Override + public void onContinueLoadingRequested(MediaPeriod source) {} + }, 0); + assertFalse(lazyPeriodPrepared.block(1)); + MediaPeriod secondLazyPeriod = mediaSource.createPeriod(new MediaPeriodId(0), null); + assertNotNull(secondLazyPeriod); + mediaSource.releasePeriod(secondLazyPeriod); + lazySources[3].triggerTimelineUpdate(createFakeTimeline(7)); waitForTimelineUpdate(); TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999); TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false); + assertTrue(lazyPeriodPrepared.block(TIMEOUT_MS)); + mediaSource.releasePeriod(lazyPeriod); mediaSource.releaseSource(); childSources[0].assertReleased(); childSources[1].assertReleased(); } + public void testEmptyTimelineMediaSource() throws InterruptedException { + timeline = null; + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( + new FakeShuffleOrder(0)); + prepareAndListenToTimelineUpdates(mediaSource); + assertNotNull(timeline); + waitForTimelineUpdate(); + TimelineAsserts.assertEmpty(timeline); + + mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY, null)); + waitForTimelineUpdate(); + TimelineAsserts.assertEmpty(timeline); + + mediaSource.addMediaSources(Arrays.asList(new MediaSource[] { + new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), + new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), + new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null) + })); + waitForTimelineUpdate(); + TimelineAsserts.assertEmpty(timeline); + + // Insert non-empty media source to leave empty sources at the start, the end, and the middle + // (with single and multiple empty sources in a row). + MediaSource[] mediaSources = createMediaSources(3); + mediaSource.addMediaSource(1, mediaSources[0]); + waitForTimelineUpdate(); + mediaSource.addMediaSource(4, mediaSources[1]); + waitForTimelineUpdate(); + mediaSource.addMediaSource(6, mediaSources[2]); + waitForTimelineUpdate(); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); + assertEquals(0, timeline.getFirstWindowIndex(false)); + assertEquals(2, timeline.getLastWindowIndex(false)); + assertEquals(2, timeline.getFirstWindowIndex(true)); + assertEquals(0, timeline.getLastWindowIndex(true)); + assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); + } + public void testIllegalArguments() { DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null); @@ -325,6 +407,28 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111)); } + private static void assertAllPeriodsCanBeCreatedPreparedAndReleased(MediaSource mediaSource, + int periodCount) { + for (int i = 0; i < periodCount; i++) { + MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(i), null); + assertNotNull(mediaPeriod); + final ConditionVariable mediaPeriodPrepared = new ConditionVariable(); + mediaPeriod.prepare(new Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + mediaPeriodPrepared.open(); + } + @Override + public void onContinueLoadingRequested(MediaPeriod source) {} + }, 0); + assertTrue(mediaPeriodPrepared.block(TIMEOUT_MS)); + MediaPeriod secondMediaPeriod = mediaSource.createPeriod(new MediaPeriodId(i), null); + assertNotNull(secondMediaPeriod); + mediaSource.releasePeriod(secondMediaPeriod); + mediaSource.releasePeriod(mediaPeriod); + } + } + private static class LazyMediaSource implements MediaSource { private Listener listener; @@ -344,7 +448,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - return null; + return new FakeMediaPeriod(TrackGroupArray.EMPTY); } @Override 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 52c313ed47..2c8deb74b4 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 @@ -42,7 +42,7 @@ public class LoopingMediaSourceTest extends TestCase { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, @@ -60,7 +60,7 @@ public class LoopingMediaSourceTest extends TestCase { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1, 1); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, @@ -80,7 +80,7 @@ public class LoopingMediaSourceTest extends TestCase { Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, 2, 0, 1); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, @@ -93,6 +93,17 @@ public class LoopingMediaSourceTest extends TestCase { } } + public void testEmptyTimelineLoop() { + Timeline timeline = getLoopingTimeline(Timeline.EMPTY, 1); + TimelineAsserts.assertEmpty(timeline); + + timeline = getLoopingTimeline(Timeline.EMPTY, 3); + TimelineAsserts.assertEmpty(timeline); + + timeline = getLoopingTimeline(Timeline.EMPTY, Integer.MAX_VALUE); + TimelineAsserts.assertEmpty(timeline); + } + /** * Wraps the specified timeline in a {@link LoopingMediaSource} and returns * the looping timeline. 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 8a1d7964ee..b83a99295a 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 @@ -606,10 +606,11 @@ public abstract class Timeline { * enabled. * * @param shuffleModeEnabled Whether shuffling is enabled. - * @return The index of the last window in the playback order. + * @return The index of the last window in the playback order, or {@link C#INDEX_UNSET} if the + * timeline is empty. */ public int getLastWindowIndex(boolean shuffleModeEnabled) { - return getWindowCount() - 1; + return isEmpty() ? C.INDEX_UNSET : getWindowCount() - 1; } /** @@ -617,10 +618,11 @@ public abstract class Timeline { * enabled. * * @param shuffleModeEnabled Whether shuffling is enabled. - * @return The index of the first window in the playback order. + * @return The index of the first window in the playback order, or {@link C#INDEX_UNSET} if the + * timeline is empty. */ public int getFirstWindowIndex(boolean shuffleModeEnabled) { - return 0; + return isEmpty() ? C.INDEX_UNSET : 0; } /** 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 07813ff046..35234753b0 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 @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.Timeline; @Override public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + // Find next window within current child. int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int nextWindowIndexInChild = getTimelineByChildIndex(childIndex).getNextWindowIndex( @@ -51,12 +52,16 @@ import com.google.android.exoplayer2.Timeline; if (nextWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + nextWindowIndexInChild; } - int nextChildIndex = shuffleModeEnabled ? shuffleOrder.getNextIndex(childIndex) - : childIndex + 1; - if (nextChildIndex != C.INDEX_UNSET && nextChildIndex < childCount) { + // If not found, find first window of next non-empty child. + int nextChildIndex = getNextChildIndex(childIndex, shuffleModeEnabled); + while (nextChildIndex != C.INDEX_UNSET && getTimelineByChildIndex(nextChildIndex).isEmpty()) { + nextChildIndex = getNextChildIndex(nextChildIndex, shuffleModeEnabled); + } + if (nextChildIndex != C.INDEX_UNSET) { return getFirstWindowIndexByChildIndex(nextChildIndex) + getTimelineByChildIndex(nextChildIndex).getFirstWindowIndex(shuffleModeEnabled); } + // If not found, this is the last window. if (repeatMode == Player.REPEAT_MODE_ALL) { return getFirstWindowIndex(shuffleModeEnabled); } @@ -66,6 +71,7 @@ import com.google.android.exoplayer2.Timeline; @Override public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + // Find previous window within current child. int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int previousWindowIndexInChild = getTimelineByChildIndex(childIndex).getPreviousWindowIndex( @@ -75,12 +81,17 @@ import com.google.android.exoplayer2.Timeline; if (previousWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + previousWindowIndexInChild; } - int previousChildIndex = shuffleModeEnabled ? shuffleOrder.getPreviousIndex(childIndex) - : childIndex - 1; - if (previousChildIndex != C.INDEX_UNSET && previousChildIndex >= 0) { + // If not found, find last window of previous non-empty child. + int previousChildIndex = getPreviousChildIndex(childIndex, shuffleModeEnabled); + while (previousChildIndex != C.INDEX_UNSET + && getTimelineByChildIndex(previousChildIndex).isEmpty()) { + previousChildIndex = getPreviousChildIndex(previousChildIndex, shuffleModeEnabled); + } + if (previousChildIndex != C.INDEX_UNSET) { return getFirstWindowIndexByChildIndex(previousChildIndex) + getTimelineByChildIndex(previousChildIndex).getLastWindowIndex(shuffleModeEnabled); } + // If not found, this is the first window. if (repeatMode == Player.REPEAT_MODE_ALL) { return getLastWindowIndex(shuffleModeEnabled); } @@ -89,14 +100,36 @@ import com.google.android.exoplayer2.Timeline; @Override public int getLastWindowIndex(boolean shuffleModeEnabled) { + if (childCount == 0) { + return C.INDEX_UNSET; + } + // Find last non-empty child. int lastChildIndex = shuffleModeEnabled ? shuffleOrder.getLastIndex() : childCount - 1; + while (getTimelineByChildIndex(lastChildIndex).isEmpty()) { + lastChildIndex = getPreviousChildIndex(lastChildIndex, shuffleModeEnabled); + if (lastChildIndex == C.INDEX_UNSET) { + // All children are empty. + return C.INDEX_UNSET; + } + } return getFirstWindowIndexByChildIndex(lastChildIndex) + getTimelineByChildIndex(lastChildIndex).getLastWindowIndex(shuffleModeEnabled); } @Override public int getFirstWindowIndex(boolean shuffleModeEnabled) { + if (childCount == 0) { + return C.INDEX_UNSET; + } + // Find first non-empty child. int firstChildIndex = shuffleModeEnabled ? shuffleOrder.getFirstIndex() : 0; + while (getTimelineByChildIndex(firstChildIndex).isEmpty()) { + firstChildIndex = getNextChildIndex(firstChildIndex, shuffleModeEnabled); + if (firstChildIndex == C.INDEX_UNSET) { + // All children are empty. + return C.INDEX_UNSET; + } + } return getFirstWindowIndexByChildIndex(firstChildIndex) + getTimelineByChildIndex(firstChildIndex).getFirstWindowIndex(shuffleModeEnabled); } @@ -196,4 +229,14 @@ import com.google.android.exoplayer2.Timeline; */ protected abstract Object getChildUidByChildIndex(int childIndex); + private int getNextChildIndex(int childIndex, boolean shuffleModeEnabled) { + return shuffleModeEnabled ? shuffleOrder.getNextIndex(childIndex) + : childIndex < childCount - 1 ? childIndex + 1 : C.INDEX_UNSET; + } + + private int getPreviousChildIndex(int childIndex, boolean shuffleModeEnabled) { + return shuffleModeEnabled ? shuffleOrder.getPreviousIndex(childIndex) + : childIndex > 0 ? childIndex - 1 : C.INDEX_UNSET; + } + } 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 0c7bcece68..4cf3843ea1 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 @@ -90,15 +90,19 @@ public final class ConcatenatingMediaSource implements MediaSource { @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { this.listener = listener; - for (int i = 0; i < mediaSources.length; i++) { - if (!duplicateFlags[i]) { - final int index = i; - mediaSources[i].prepareSource(player, false, new Listener() { - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - handleSourceInfoRefreshed(index, timeline, manifest); - } - }); + if (mediaSources.length == 0) { + listener.onSourceInfoRefreshed(Timeline.EMPTY, null); + } else { + for (int i = 0; i < mediaSources.length; i++) { + if (!duplicateFlags[i]) { + final int index = i; + mediaSources[i].prepareSource(player, false, new Listener() { + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + handleSourceInfoRefreshed(index, timeline, manifest); + } + }); + } } } } @@ -245,12 +249,12 @@ public final class ConcatenatingMediaSource implements MediaSource { @Override protected int getChildIndexByPeriodIndex(int periodIndex) { - return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex, true, false) + 1; + return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex + 1, false, false) + 1; } @Override protected int getChildIndexByWindowIndex(int windowIndex) { - return Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1; + return Util.binarySearchFloor(sourceWindowOffsets, windowIndex + 1, false, false) + 1; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 9c1e7ec1ba..8614cf9c85 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -395,7 +395,14 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl private int findMediaSourceHolderByPeriodIndex(int periodIndex) { query.firstPeriodIndexInChild = periodIndex; int index = Collections.binarySearch(mediaSourceHolders, query); - return index >= 0 ? index : -index - 2; + if (index < 0) { + return -index - 2; + } + while (index < mediaSourceHolders.size() - 1 + && mediaSourceHolders.get(index + 1).firstPeriodIndexInChild == periodIndex) { + index++; + } + return index; } private static final class MediaSourceHolder implements Comparable { @@ -456,12 +463,12 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl @Override protected int getChildIndexByPeriodIndex(int periodIndex) { - return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex, true, false); + return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false); } @Override protected int getChildIndexByWindowIndex(int windowIndex) { - return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex, true, false); + return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false); } @Override 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 00e3c50506..b23b36dcf3 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 @@ -107,8 +107,10 @@ 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"); + if (childPeriodCount > 0) { + Assertions.checkState(loopCount <= Integer.MAX_VALUE / childPeriodCount, + "LoopingMediaSource contains too many periods"); + } } @Override 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 74129a0e69..c61aac708c 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 @@ -36,6 +36,10 @@ public final class TimelineAsserts { public static void assertEmpty(Timeline timeline) { assertWindowIds(timeline); assertPeriodCounts(timeline); + for (boolean shuffled : new boolean[] {false, true}) { + assertEquals(C.INDEX_UNSET, timeline.getFirstWindowIndex(shuffled)); + assertEquals(C.INDEX_UNSET, timeline.getLastWindowIndex(shuffled)); + } } /** @@ -119,8 +123,9 @@ public final class TimelineAsserts { expectedWindowIndex++; } assertEquals(expectedWindowIndex, period.windowIndex); + assertEquals(i, timeline.getIndexOfPeriod(period.uid)); for (@Player.RepeatMode int repeatMode - : new int[] { Player.REPEAT_MODE_OFF, Player.REPEAT_MODE_ONE, Player.REPEAT_MODE_ALL }) { + : new int[] {Player.REPEAT_MODE_OFF, Player.REPEAT_MODE_ONE, Player.REPEAT_MODE_ALL}) { if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) { assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, repeatMode, false)); } else { From cc58b515b773be93ba4572d5ac221cae856ee1ae Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 25 Aug 2017 09:35:10 -0700 Subject: [PATCH 0022/1327] Make Downloaders open source ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166486294 --- .../google/android/exoplayer2/offline/DownloadException.java | 2 -- .../java/com/google/android/exoplayer2/offline/Downloader.java | 2 -- .../exoplayer2/offline/DownloaderConstructorHelper.java | 2 -- .../android/exoplayer2/offline/ProgressiveDownloader.java | 2 -- .../google/android/exoplayer2/offline/SegmentDownloader.java | 2 -- .../exoplayer2/source/hls/offline/HlsDownloadTestData.java | 3 --- .../exoplayer2/source/hls/offline/HlsDownloaderTest.java | 2 -- .../android/exoplayer2/source/hls/offline/HlsDownloader.java | 2 -- .../source/smoothstreaming/offline/SsDownloader.java | 2 -- .../android/exoplayer2/playbacktests/gts/DashDownloadTest.java | 2 -- 10 files changed, 21 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java index 239195892c..730ce2d3e8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java @@ -15,11 +15,9 @@ */ package com.google.android.exoplayer2.offline; -import com.google.android.exoplayer2.util.ClosedSource; import java.io.IOException; /** Thrown on an error during downloading. */ -@ClosedSource(reason = "Not ready yet") public final class DownloadException extends IOException { /** @param message The message for the exception. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java index a130bb4052..b8d9432c63 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java @@ -17,13 +17,11 @@ package com.google.android.exoplayer2.offline; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.ClosedSource; import java.io.IOException; /** * An interface for stream downloaders. */ -@ClosedSource(reason = "Not ready yet") public interface Downloader { /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java index 5f9a4d973a..9ef9366397 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java @@ -27,11 +27,9 @@ 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.Assertions; -import com.google.android.exoplayer2.util.ClosedSource; import com.google.android.exoplayer2.util.PriorityTaskManager; /** A helper class that holds necessary parameters for {@link Downloader} construction. */ -@ClosedSource(reason = "Not ready yet") public final class DownloaderConstructorHelper { private final Cache cache; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java index c6bb3bc432..e5aa429424 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java @@ -23,14 +23,12 @@ 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 com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; -import com.google.android.exoplayer2.util.ClosedSource; import com.google.android.exoplayer2.util.PriorityTaskManager; import java.io.IOException; /** * A downloader for progressive media streams. */ -@ClosedSource(reason = "Not ready yet") public final class ProgressiveDownloader implements Downloader { private static final int BUFFER_SIZE_BYTES = 128 * 1024; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java index 93e7c57470..d81df90b81 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -25,7 +25,6 @@ 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 com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; -import com.google.android.exoplayer2.util.ClosedSource; import com.google.android.exoplayer2.util.PriorityTaskManager; import java.io.IOException; import java.util.Collections; @@ -40,7 +39,6 @@ import java.util.List; * @param The type of the manifest object. * @param The type of the representation key object. */ -@ClosedSource(reason = "Not ready yet") public abstract class SegmentDownloader implements Downloader { /** Smallest unit of content to be downloaded. */ diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java index 133bf19dba..ec70fb1200 100644 --- a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java @@ -15,12 +15,9 @@ */ package com.google.android.exoplayer2.source.hls.offline; -import com.google.android.exoplayer2.util.ClosedSource; - /** * Data for HLS downloading tests. */ -@ClosedSource(reason = "Not ready yet") /* package */ interface HlsDownloadTestData { String MASTER_PLAYLIST_URI = "test.m3u8"; diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java index 28afe450eb..ebf73ebfd7 100644 --- a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java @@ -39,12 +39,10 @@ import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; -import com.google.android.exoplayer2.util.ClosedSource; import com.google.android.exoplayer2.util.Util; import java.io.File; /** Unit tests for {@link HlsDownloader}. */ -@ClosedSource(reason = "Not ready yet") public class HlsDownloaderTest extends InstrumentationTestCase { private SimpleCache cache; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java index 488b85e78a..ac8ec5ee5e 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.ParsingLoadable; -import com.google.android.exoplayer2.util.ClosedSource; import com.google.android.exoplayer2.util.UriUtil; import java.io.IOException; import java.util.ArrayList; @@ -41,7 +40,6 @@ import java.util.List; * #selectRepresentations(Object[])}. As key, string form of the rendition's url is used. The urls * can be absolute or relative to the master playlist url. */ -@ClosedSource(reason = "Not ready yet") public final class HlsDownloader extends SegmentDownloader { /** diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java index fe9c21d855..21cacdc6f3 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java @@ -26,7 +26,6 @@ import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.ParsingLoadable; -import com.google.android.exoplayer2.util.ClosedSource; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -60,7 +59,6 @@ import java.util.List; * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);} * */ -@ClosedSource(reason = "Not ready yet") public final class SsDownloader extends SegmentDownloader { /** diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java index 706dd72166..66884f3e5b 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java @@ -32,7 +32,6 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; -import com.google.android.exoplayer2.util.ClosedSource; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; @@ -43,7 +42,6 @@ import java.util.List; /** * Tests downloaded DASH playbacks. */ -@ClosedSource(reason = "Not ready yet") public final class DashDownloadTest extends ActivityInstrumentationTestCase2 { private static final String TAG = "DashDownloadTest"; From ba6d208fe95b9c9eef431fdd6f6185794154e1a7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 29 Aug 2017 02:15:08 -0700 Subject: [PATCH 0023/1327] Use Math.abs in Sonic.java ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166820970 --- .../main/java/com/google/android/exoplayer2/audio/Sonic.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ef7877ae1e..5c5ac06da3 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 @@ -241,7 +241,7 @@ import java.util.Arrays; for (int i = 0; i < period; i++) { short sVal = samples[position + i]; short pVal = samples[position + period + i]; - diff += sVal >= pVal ? sVal - pVal : pVal - sVal; + diff += Math.abs(sVal - pVal); } // Note that the highest number of samples we add into diff will be less than 256, since we // skip samples. Thus, diff is a 24 bit number, and we can safely multiply by numSamples From 3e762270062726a690be50d285474175c81ae91f Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 07:17:24 -0700 Subject: [PATCH 0024/1327] Don't copy primary-track format to non-primary tracks Copying non-primary-track formats to non-primary tracks looks non-trivial (I tried; went down a dead-end), so leaving that for now. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166843123 --- .../source/chunk/BaseMediaChunkOutput.java | 27 +++++++++++++++---- .../source/chunk/ChunkExtractorWrapper.java | 24 ++++++++++++----- .../source/chunk/ChunkSampleStream.java | 10 ++----- 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java index 9531aaf32e..0b6c196d7c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -32,12 +33,23 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut private final SampleQueue[] sampleQueues; /** - * @param trackTypes The track types of the individual track outputs. - * @param sampleQueues The individual sample queues. + * @param primaryTrackType The type of the primary track. + * @param primarySampleQueue The primary track sample queues. + * @param embeddedTrackTypes The types of any embedded tracks, or null. + * @param embeddedSampleQueues The track sample queues for any embedded tracks, or null. */ - public BaseMediaChunkOutput(int[] trackTypes, SampleQueue[] sampleQueues) { - this.trackTypes = trackTypes; - this.sampleQueues = sampleQueues; + @SuppressWarnings("ConstantConditions") + public BaseMediaChunkOutput(int primaryTrackType, SampleQueue primarySampleQueue, + @Nullable int[] embeddedTrackTypes, @Nullable SampleQueue[] embeddedSampleQueues) { + int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; + trackTypes = new int[1 + embeddedTrackCount]; + sampleQueues = new SampleQueue[1 + embeddedTrackCount]; + trackTypes[0] = primaryTrackType; + sampleQueues[0] = primarySampleQueue; + for (int i = 0; i < embeddedTrackCount; i++) { + trackTypes[i + 1] = embeddedTrackTypes[i]; + sampleQueues[i + 1] = embeddedSampleQueues[i]; + } } @Override @@ -51,6 +63,11 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut return new DummyTrackOutput(); } + @Override + public boolean isPrimaryTrack(int type) { + return type == trackTypes[0]; + } + /** * Returns the current absolute write indices of the individual sample queues. */ 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 07d1cce8cb..eda9ed3cf7 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 @@ -45,13 +45,22 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { *

      * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}. * - * @param id A track identifier. - * @param type The type of the track. Typically one of the - * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. + * @param id The track identifier. + * @param type The track type. Typically one of the {@link com.google.android.exoplayer2.C} + * {@code TRACK_TYPE_*} constants. * @return The {@link TrackOutput} for the given track identifier. */ TrackOutput track(int id, int type); + /** + * Returns whether the specified type corresponds to the primary track. + * + * @param type The track type. Typically one of the {@link com.google.android.exoplayer2.C} + * {@code TRACK_TYPE_*} constants. + * @return Whether {@code type} corresponds to the primary track. + */ + boolean isPrimaryTrack(int type); + } public final Extractor extractor; @@ -146,6 +155,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { private final Format manifestFormat; public Format sampleFormat; + private boolean isPrimaryTrack; private TrackOutput trackOutput; public BindingTrackOutput(int id, int type, Format manifestFormat) { @@ -159,17 +169,17 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { trackOutput = new DummyTrackOutput(); return; } + isPrimaryTrack = trackOutputProvider.isPrimaryTrack(type); trackOutput = trackOutputProvider.track(id, type); - if (trackOutput != null) { + if (sampleFormat != null) { trackOutput.format(sampleFormat); } } @Override public void format(Format format) { - // TODO: This should only happen for the primary track. Additional metadata/text tracks need - // to be copied with different manifest derived formats. - sampleFormat = format.copyWithManifestFormatInfo(manifestFormat); + // TODO: Non-primary tracks should be copied with data from their own manifest formats. + sampleFormat = isPrimaryTrack ? format.copyWithManifestFormatInfo(manifestFormat) : format; trackOutput.format(sampleFormat); } 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 f2609a0ffd..e8586f7230 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 @@ -87,21 +87,15 @@ public class ChunkSampleStream implements SampleStream, S int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; embeddedSampleQueues = new SampleQueue[embeddedTrackCount]; embeddedTracksSelected = new boolean[embeddedTrackCount]; - int[] trackTypes = new int[1 + embeddedTrackCount]; - SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount]; primarySampleQueue = new SampleQueue(allocator); - trackTypes[0] = primaryTrackType; - sampleQueues[0] = primarySampleQueue; - for (int i = 0; i < embeddedTrackCount; i++) { SampleQueue sampleQueue = new SampleQueue(allocator); embeddedSampleQueues[i] = sampleQueue; - sampleQueues[i + 1] = sampleQueue; - trackTypes[i + 1] = embeddedTrackTypes[i]; } - mediaChunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues); + mediaChunkOutput = new BaseMediaChunkOutput(primaryTrackType, primarySampleQueue, + embeddedTrackTypes, embeddedSampleQueues); pendingResetPositionUs = positionUs; lastSeekPositionUs = positionUs; } From f44e30c7543e63bb8aec54f27626d4c70e808c48 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 07:19:48 -0700 Subject: [PATCH 0025/1327] Fix mapping CLEARKEY_UUID to COMMON_PSSH_UUID This mapping when we call into platform components also needs to be applied when creating the MediaCrypto instance. The fix is to stop propagating the UUID through all the createMediaCrypto methods. This is unnecessary, since the eventual target already knows its own UUID! Issue: #3138 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166843372 --- .../android/exoplayer2/drm/DefaultDrmSession.java | 2 +- .../google/android/exoplayer2/drm/DrmInitData.java | 2 +- .../google/android/exoplayer2/drm/ExoMediaDrm.java | 4 +--- .../android/exoplayer2/drm/FrameworkMediaDrm.java | 11 +++++------ 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index cfb2cf9d8a..b4dab7b971 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -226,7 +226,7 @@ import java.util.UUID; try { sessionId = mediaDrm.openSession(); - mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId); + mediaCrypto = mediaDrm.createMediaCrypto(sessionId); state = STATE_OPENED; return true; } catch (NotProvisionedException e) { 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 9fa6547a00..d814ece31d 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 @@ -60,7 +60,7 @@ public final class DrmInitData implements Comparator, Parcelable { if (cloneSchemeDatas) { schemeDatas = schemeDatas.clone(); } - // Sorting ensures that universal scheme data(i.e. data that applies to all schemes) is matched + // Sorting ensures that universal scheme data (i.e. data that applies to all schemes) is matched // last. It's also required by the equals and hashcode implementations. Arrays.sort(schemeDatas, this); // Check for no duplicates. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java index 3d765dbef5..63387f19e1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java @@ -22,7 +22,6 @@ import android.media.NotProvisionedException; import android.media.ResourceBusyException; import java.util.HashMap; import java.util.Map; -import java.util.UUID; /** * Used to obtain keys for decrypting protected media streams. See {@link android.media.MediaDrm}. @@ -137,11 +136,10 @@ public interface ExoMediaDrm { /** * @see android.media.MediaCrypto#MediaCrypto(UUID, byte[]) * - * @param uuid The UUID of the crypto scheme. * @param initData Opaque initialization data specific to the crypto scheme. * @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data. * @throws MediaCryptoException */ - T createMediaCrypto(UUID uuid, byte[] initData) throws MediaCryptoException; + T createMediaCrypto(byte[] initData) throws MediaCryptoException; } 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 5d0cb038d4..d664cb69a9 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 @@ -37,6 +37,7 @@ import java.util.UUID; @TargetApi(18) public final class FrameworkMediaDrm implements ExoMediaDrm { + private final UUID uuid; private final MediaDrm mediaDrm; /** @@ -59,10 +60,9 @@ public final class FrameworkMediaDrm implements ExoMediaDrm Date: Tue, 29 Aug 2017 08:16:11 -0700 Subject: [PATCH 0026/1327] Add media queue support to CastPlayer Also workaround the non-repeatable queue and fix other minor issues. Issue:#2283 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166848894 --- .../exoplayer2/castdemo/CastDemoUtil.java | 8 +- .../exoplayer2/castdemo/PlayerManager.java | 32 +- .../src/main/res/layout/main_activity.xml | 5 +- .../exoplayer2/ext/cast/CastPlayer.java | 520 ++++++++++++------ .../exoplayer2/ext/cast/CastTimeline.java | 114 ++++ .../DynamicConcatenatingMediaSource.java | 2 +- 6 files changed, 494 insertions(+), 187 deletions(-) create mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java index f819e54e50..68c5904362 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java @@ -52,17 +52,17 @@ import java.util.List; /** * The mime type of the media sample, as required by {@link MediaInfo#setContentType}. */ - public final String type; + public final String mimeType; /** * @param uri See {@link #uri}. * @param name See {@link #name}. - * @param type See {@link #type}. + * @param mimeType See {@link #mimeType}. */ - public Sample(String uri, String name, String type) { + public Sample(String uri, String name, String mimeType) { this.uri = uri; this.name = name; - this.type = type; + this.mimeType = mimeType; } @Override diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 741df7eff1..8b461ec65c 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -37,6 +37,9 @@ import com.google.android.exoplayer2.ui.PlaybackControlView; import com.google.android.exoplayer2.ui.SimpleExoPlayerView; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.gms.cast.MediaInfo; +import com.google.android.gms.cast.MediaMetadata; +import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; /** @@ -95,12 +98,12 @@ import com.google.android.gms.cast.framework.CastContext; boolean playWhenReady) { this.currentSample = currentSample; if (playbackLocation == PLAYBACK_REMOTE) { - castPlayer.load(currentSample.name, currentSample.uri, currentSample.type, positionMs, - playWhenReady); + castPlayer.loadItem(buildMediaQueueItem(currentSample), positionMs); + castPlayer.setPlayWhenReady(playWhenReady); } else /* playbackLocation == PLAYBACK_LOCAL */ { + exoPlayer.prepare(buildMediaSource(currentSample), true, true); exoPlayer.setPlayWhenReady(playWhenReady); exoPlayer.seekTo(positionMs); - exoPlayer.prepare(buildMediaSource(currentSample), true, true); } } @@ -143,9 +146,18 @@ import com.google.android.gms.cast.framework.CastContext; // Internal methods. + private static MediaQueueItem buildMediaQueueItem(CastDemoUtil.Sample sample) { + MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); + movieMetadata.putString(MediaMetadata.KEY_TITLE, sample.name); + MediaInfo mediaInfo = new MediaInfo.Builder(sample.uri) + .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED).setContentType(sample.mimeType) + .setMetadata(movieMetadata).build(); + return new MediaQueueItem.Builder(mediaInfo).build(); + } + private static MediaSource buildMediaSource(CastDemoUtil.Sample sample) { Uri uri = Uri.parse(sample.uri); - switch (sample.type) { + switch (sample.mimeType) { case CastDemoUtil.MIME_TYPE_SS: return new SsMediaSource(uri, DATA_SOURCE_FACTORY, new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), null, null); @@ -158,7 +170,7 @@ import com.google.android.gms.cast.framework.CastContext; return new ExtractorMediaSource(uri, DATA_SOURCE_FACTORY, new DefaultExtractorsFactory(), null, null); default: { - throw new IllegalStateException("Unsupported type: " + sample.type); + throw new IllegalStateException("Unsupported type: " + sample.mimeType); } } } @@ -177,14 +189,16 @@ import com.google.android.gms.cast.framework.CastContext; castControlView.show(); } - long playbackPositionMs = 0; - boolean playWhenReady = true; - if (exoPlayer != null) { + long playbackPositionMs; + boolean playWhenReady; + if (this.playbackLocation == PLAYBACK_LOCAL) { playbackPositionMs = exoPlayer.getCurrentPosition(); playWhenReady = exoPlayer.getPlayWhenReady(); - } else if (this.playbackLocation == PLAYBACK_REMOTE) { + exoPlayer.stop(); + } else /* this.playbackLocation == PLAYBACK_REMOTE */ { playbackPositionMs = castPlayer.getCurrentPosition(); playWhenReady = castPlayer.getPlayWhenReady(); + castPlayer.stop(); } this.playbackLocation = playbackLocation; diff --git a/demos/cast/src/main/res/layout/main_activity.xml b/demos/cast/src/main/res/layout/main_activity.xml index 7e39320e3b..5d94931b64 100644 --- a/demos/cast/src/main/res/layout/main_activity.xml +++ b/demos/cast/src/main/res/layout/main_activity.xml @@ -35,7 +35,8 @@ android:id="@+id/cast_control_view" android:layout_width="match_parent" android:layout_height="0dp" - app:show_timeout="-1" android:layout_weight="2" - android:visibility="gone"/> + android:visibility="gone" + app:repeat_toggle_modes="all|one" + app:show_timeout="-1"/> diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index e79fef74d5..234b8384f9 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -15,23 +15,24 @@ */ package com.google.android.exoplayer2.ext.cast; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.FixedTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import com.google.android.gms.cast.CastStatusCodes; import com.google.android.gms.cast.MediaInfo; -import com.google.android.gms.cast.MediaMetadata; +import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.MediaTrack; import com.google.android.gms.cast.framework.CastContext; @@ -41,6 +42,7 @@ import com.google.android.gms.cast.framework.SessionManagerListener; import com.google.android.gms.cast.framework.media.RemoteMediaClient; import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult; import com.google.android.gms.common.api.CommonStatusCodes; +import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; @@ -48,19 +50,16 @@ import java.util.concurrent.CopyOnWriteArraySet; /** * {@link Player} implementation that communicates with a Cast receiver app. * - *

      Calls to the methods in this class depend on the availability of an underlying cast session. - * If no session is available, method calls have no effect. To keep track of the underyling session, + *

      The behavior of this class depends on the underlying Cast session, which is obtained from the + * Cast context passed to {@link #CastPlayer}. To keep track of the session, * {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be - * implemented and attached to the player. + * implemented and attached to the player.

      * - *

      Methods should be called on the application's main thread. + *

      If no session is available, the player state will remain unchanged and calls to methods that + * alter it will be ignored. Querying the player state is possible even when no session is + * available, in which case, the last observed receiver app state is reported.

      * - *

      Known issues: - *

        - *
      • Part of the Cast API is not exposed through this interface. For instance, volume settings - * and track selection.
      • - *
      • Repeat mode is not working. See [internal: b/64137174].
      • - *
      + *

      Methods should be called on the application's main thread.

      */ public final class CastPlayer implements Player { @@ -95,10 +94,12 @@ public final class CastPlayer implements Player { private final CastContext castContext; private final Timeline.Window window; + private final Timeline.Period period; + + private RemoteMediaClient remoteMediaClient; // Result callbacks. private final StatusListener statusListener; - private final RepeatModeResultCallback repeatModeResultCallback; private final SeekResultCallback seekResultCallback; // Listeners. @@ -106,11 +107,15 @@ public final class CastPlayer implements Player { private SessionAvailabilityListener sessionAvailabilityListener; // Internal state. - private RemoteMediaClient remoteMediaClient; - private Timeline currentTimeline; + private CastTimeline currentTimeline; private TrackGroupArray currentTrackGroups; private TrackSelectionArray currentTrackSelection; + private int playbackState; + private int repeatMode; + private int currentWindowIndex; + private boolean playWhenReady; private long lastReportedPositionMs; + private int pendingSeekWindowIndex; private long pendingSeekPositionMs; /** @@ -119,41 +124,142 @@ public final class CastPlayer implements Player { public CastPlayer(CastContext castContext) { this.castContext = castContext; window = new Timeline.Window(); + period = new Timeline.Period(); statusListener = new StatusListener(); - repeatModeResultCallback = new RepeatModeResultCallback(); seekResultCallback = new SeekResultCallback(); listeners = new CopyOnWriteArraySet<>(); + SessionManager sessionManager = castContext.getSessionManager(); sessionManager.addSessionManagerListener(statusListener, CastSession.class); CastSession session = sessionManager.getCurrentCastSession(); remoteMediaClient = session != null ? session.getRemoteMediaClient() : null; + + playbackState = STATE_IDLE; + repeatMode = REPEAT_MODE_OFF; + currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE; + currentTrackGroups = EMPTY_TRACK_GROUP_ARRAY; + currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY; + pendingSeekWindowIndex = C.INDEX_UNSET; pendingSeekPositionMs = C.TIME_UNSET; updateInternalState(); } + // Media Queue manipulation methods. + /** - * Loads media into the receiver app. + * Loads a single item media queue. If no session is available, does nothing. * - * @param title The title of the media sample. - * @param url The url from which the media is obtained. - * @param contentMimeType The mime type of the content to play. - * @param positionMs The position at which the playback should start in milliseconds. - * @param playWhenReady Whether the player should start playback as soon as it is ready to do so. + * @param item The item to load. + * @param positionMs The position at which the playback should start in milliseconds relative to + * the start of the item at {@code startIndex}. + * @return The Cast {@code PendingResult}, or null if no session is available. */ - public void load(String title, String url, String contentMimeType, long positionMs, - boolean playWhenReady) { - lastReportedPositionMs = 0; - if (remoteMediaClient != null) { - MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); - movieMetadata.putString(MediaMetadata.KEY_TITLE, title); - MediaInfo mediaInfo = new MediaInfo.Builder(url).setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setContentType(contentMimeType).setMetadata(movieMetadata).build(); - remoteMediaClient.load(mediaInfo, playWhenReady, positionMs); - } + public PendingResult loadItem(MediaQueueItem item, long positionMs) { + return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF); } /** - * Returns whether a cast session is available for playback. + * Loads a media queue. If no session is available, does nothing. + * + * @param items The items to load. + * @param startIndex The index of the item at which playback should start. + * @param positionMs The position at which the playback should start in milliseconds relative to + * the start of the item at {@code startIndex}. + * @param repeatMode The repeat mode for the created media queue. + * @return The Cast {@code PendingResult}, or null if no session is available. + */ + public PendingResult loadItems(MediaQueueItem[] items, int startIndex, + long positionMs, @RepeatMode int repeatMode) { + if (remoteMediaClient != null) { + return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode), + positionMs, null); + } + return null; + } + + /** + * Appends a sequence of items to the media queue. If no media queue exists, does nothing. + * + * @param items The items to append. + * @return The Cast {@code PendingResult}, or null if no media queue exists. + */ + public PendingResult addItems(MediaQueueItem... items) { + return addItems(MediaQueueItem.INVALID_ITEM_ID, items); + } + + /** + * Inserts a sequence of items into the media queue. If no media queue or period with id + * {@code periodId} exist, does nothing. + * + * @param periodId The id of the period ({@see #getCurrentTimeline}) that corresponds to the item + * that will follow immediately after the inserted items. + * @param items The items to insert. + * @return The Cast {@code PendingResult}, or null if no media queue or no period with id + * {@code periodId} exist. + */ + public PendingResult addItems(int periodId, MediaQueueItem... items) { + if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID + || currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) { + return remoteMediaClient.queueInsertItems(items, periodId, null); + } + return null; + } + + /** + * Removes an item from the media queue. If no media queue or period with id {@code periodId} + * exist, does nothing. + * + * @param periodId The id of the period ({@see #getCurrentTimeline}) that corresponds to the item + * to remove. + * @return The Cast {@code PendingResult}, or null if no media queue or no period with id + * {@code periodId} exist. + */ + public PendingResult removeItem(int periodId) { + if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { + return remoteMediaClient.queueRemoveItem(periodId, null); + } + return null; + } + + /** + * Moves an existing item within the media queue. If no media queue or period with id + * {@code periodId} exist, does nothing. + * + * @param periodId The id of the period ({@see #getCurrentTimeline}) that corresponds to the item + * to move. + * @param newIndex The target index of the item in the media queue. Must be in the range + * 0 <= index < {@link Timeline#getPeriodCount()}, as provided by + * {@link #getCurrentTimeline()}. + * @return The Cast {@code PendingResult}, or null if no media queue or no period with id + * {@code periodId} exist. + */ + public PendingResult moveItem(int periodId, int newIndex) { + Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount()); + if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { + return remoteMediaClient.queueMoveItemToNewIndex(periodId, newIndex, null); + } + return null; + } + + /** + * Returns the item that corresponds to the period with the given id, or null if no media queue or + * period with id {@code periodId} exist. + * + * @param periodId The id of the period ({@see #getCurrentTimeline}) that corresponds to the item + * to get. + * @return The item that corresponds to the period with the given id, or null if no media queue or + * period with id {@code periodId} exist. + */ + public MediaQueueItem getItem(int periodId) { + MediaStatus mediaStatus = getMediaStatus(); + return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET + ? mediaStatus.getItemById(periodId) : null; + } + + // CastSession methods. + + /** + * Returns whether a cast session is available. */ public boolean isCastSessionAvailable() { return remoteMediaClient != null; @@ -182,21 +288,7 @@ public final class CastPlayer implements Player { @Override public int getPlaybackState() { - if (remoteMediaClient == null) { - return STATE_IDLE; - } - int receiverAppStatus = remoteMediaClient.getPlayerState(); - switch (receiverAppStatus) { - case MediaStatus.PLAYER_STATE_BUFFERING: - return STATE_BUFFERING; - case MediaStatus.PLAYER_STATE_PLAYING: - case MediaStatus.PLAYER_STATE_PAUSED: - return STATE_READY; - case MediaStatus.PLAYER_STATE_IDLE: - case MediaStatus.PLAYER_STATE_UNKNOWN: - default: - return STATE_IDLE; - } + return playbackState; } @Override @@ -213,7 +305,7 @@ public final class CastPlayer implements Player { @Override public boolean getPlayWhenReady() { - return remoteMediaClient != null && !remoteMediaClient.isPaused(); + return playWhenReady; } @Override @@ -228,13 +320,20 @@ public final class CastPlayer implements Player { @Override public void seekTo(long positionMs) { - seekTo(0, positionMs); + seekTo(getCurrentWindowIndex(), positionMs); } @Override public void seekTo(int windowIndex, long positionMs) { - if (remoteMediaClient != null) { - remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback); + MediaStatus mediaStatus = getMediaStatus(); + if (mediaStatus != null) { + if (getCurrentWindowIndex() != windowIndex) { + remoteMediaClient.queueJumpToItem((int) currentTimeline.getPeriod(windowIndex, period).uid, + positionMs, null).setResultCallback(seekResultCallback); + } else { + remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback); + } + pendingSeekWindowIndex = windowIndex; pendingSeekPositionMs = positionMs; for (EventListener listener : listeners) { listener.onPositionDiscontinuity(); @@ -287,47 +386,13 @@ public final class CastPlayer implements Player { @Override public void setRepeatMode(@RepeatMode int repeatMode) { if (remoteMediaClient != null) { - int castRepeatMode; - switch (repeatMode) { - case REPEAT_MODE_ONE: - castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_SINGLE; - break; - case REPEAT_MODE_ALL: - castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_ALL; - break; - case REPEAT_MODE_OFF: - castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_OFF; - break; - default: - throw new IllegalArgumentException(); - } - remoteMediaClient.queueSetRepeatMode(castRepeatMode, null) - .setResultCallback(repeatModeResultCallback); + remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), null); } } @Override @RepeatMode public int getRepeatMode() { - if (remoteMediaClient == null) { - return REPEAT_MODE_OFF; - } - MediaStatus mediaStatus = getMediaStatus(); - if (mediaStatus == null) { - // No media session active, yet. - return REPEAT_MODE_OFF; - } - int castRepeatMode = mediaStatus.getQueueRepeatMode(); - switch (castRepeatMode) { - case MediaStatus.REPEAT_MODE_REPEAT_SINGLE: - return REPEAT_MODE_ONE; - case MediaStatus.REPEAT_MODE_REPEAT_ALL: - case MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE: - return REPEAT_MODE_ALL; - case MediaStatus.REPEAT_MODE_REPEAT_OFF: - return REPEAT_MODE_OFF; - default: - throw new IllegalStateException(); - } + return repeatMode; } @Override @@ -363,12 +428,12 @@ public final class CastPlayer implements Player { @Override public int getCurrentPeriodIndex() { - return 0; + return getCurrentWindowIndex(); } @Override public int getCurrentWindowIndex() { - return 0; + return pendingSeekWindowIndex != C.INDEX_UNSET ? pendingSeekWindowIndex : currentWindowIndex; } @Override @@ -384,14 +449,14 @@ public final class CastPlayer implements Player { @Override public long getDuration() { return currentTimeline.isEmpty() ? C.TIME_UNSET - : currentTimeline.getWindow(0, window).getDurationMs(); + : currentTimeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); } @Override public long getCurrentPosition() { - return remoteMediaClient == null ? lastReportedPositionMs - : pendingSeekPositionMs != C.TIME_UNSET ? pendingSeekPositionMs - : remoteMediaClient.getApproximateStreamPosition(); + return pendingSeekPositionMs != C.TIME_UNSET ? pendingSeekPositionMs + : remoteMediaClient != null ? remoteMediaClient.getApproximateStreamPosition() + : lastReportedPositionMs; } @Override @@ -447,6 +512,121 @@ public final class CastPlayer implements Player { // Internal methods. + public void updateInternalState() { + if (remoteMediaClient == null) { + // There is no session. We leave the state of the player as it is now. + return; + } + + int playbackState = fetchPlaybackState(remoteMediaClient); + boolean playWhenReady = !remoteMediaClient.isPaused(); + if (this.playbackState != playbackState + || this.playWhenReady != playWhenReady) { + this.playbackState = playbackState; + this.playWhenReady = playWhenReady; + for (EventListener listener : listeners) { + listener.onPlayerStateChanged(this.playWhenReady, this.playbackState); + } + } + @RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient); + if (this.repeatMode != repeatMode) { + this.repeatMode = repeatMode; + for (EventListener listener : listeners) { + listener.onRepeatModeChanged(repeatMode); + } + } + int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus()); + if (this.currentWindowIndex != currentWindowIndex) { + this.currentWindowIndex = currentWindowIndex; + for (EventListener listener : listeners) { + listener.onPositionDiscontinuity(); + } + } + if (updateTracksAndSelections()) { + for (EventListener listener : listeners) { + listener.onTracksChanged(currentTrackGroups, currentTrackSelection); + } + } + maybeUpdateTimelineAndNotify(); + } + + private void maybeUpdateTimelineAndNotify() { + if (updateTimeline()) { + for (EventListener listener : listeners) { + listener.onTimelineChanged(currentTimeline, null); + } + } + } + + /** + * Updates the current timeline and returns whether it has changed. + */ + private boolean updateTimeline() { + MediaStatus mediaStatus = getMediaStatus(); + if (mediaStatus == null) { + boolean hasChanged = currentTimeline != CastTimeline.EMPTY_CAST_TIMELINE; + currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE; + return hasChanged; + } + + List items = mediaStatus.getQueueItems(); + if (!currentTimeline.represents(items)) { + currentTimeline = !items.isEmpty() ? new CastTimeline(mediaStatus.getQueueItems()) + : CastTimeline.EMPTY_CAST_TIMELINE; + return true; + } + return false; + } + + /** + * Updates the internal tracks and selection and returns whether they have changed. + */ + private boolean updateTracksAndSelections() { + if (remoteMediaClient == null) { + // There is no session. We leave the state of the player as it is now. + return false; + } + + MediaStatus mediaStatus = getMediaStatus(); + MediaInfo mediaInfo = mediaStatus != null ? mediaStatus.getMediaInfo() : null; + List castMediaTracks = mediaInfo != null ? mediaInfo.getMediaTracks() : null; + if (castMediaTracks == null || castMediaTracks.isEmpty()) { + boolean hasChanged = currentTrackGroups != EMPTY_TRACK_GROUP_ARRAY; + currentTrackGroups = EMPTY_TRACK_GROUP_ARRAY; + currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY; + return hasChanged; + } + long[] activeTrackIds = mediaStatus.getActiveTrackIds(); + if (activeTrackIds == null) { + activeTrackIds = EMPTY_TRACK_ID_ARRAY; + } + + TrackGroup[] trackGroups = new TrackGroup[castMediaTracks.size()]; + TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT]; + for (int i = 0; i < castMediaTracks.size(); i++) { + MediaTrack mediaTrack = castMediaTracks.get(i); + trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack)); + + long id = mediaTrack.getId(); + int trackType = MimeTypes.getTrackType(mediaTrack.getContentType()); + int rendererIndex = getRendererIndexForTrackType(trackType); + if (isTrackActive(id, activeTrackIds) && rendererIndex != C.INDEX_UNSET + && trackSelections[rendererIndex] == null) { + trackSelections[rendererIndex] = new FixedTrackSelection(trackGroups[i], 0); + } + } + TrackGroupArray newTrackGroups = new TrackGroupArray(trackGroups); + TrackSelectionArray newTrackSelections = new TrackSelectionArray(trackSelections); + + if (!newTrackGroups.equals(currentTrackGroups) + || !newTrackSelections.equals(currentTrackSelection)) { + currentTrackSelection = new TrackSelectionArray(trackSelections); + currentTrackGroups = new TrackGroupArray(trackGroups); + return true; + } + return false; + } + private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) { if (this.remoteMediaClient == remoteMediaClient) { // Do nothing. @@ -463,6 +643,7 @@ public final class CastPlayer implements Player { } remoteMediaClient.addListener(statusListener); remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS); + updateInternalState(); } else { if (sessionAvailabilityListener != null) { sessionAvailabilityListener.onCastSessionUnavailable(); @@ -474,50 +655,58 @@ public final class CastPlayer implements Player { return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null; } - private @Nullable MediaInfo getMediaInfo() { - return remoteMediaClient != null ? remoteMediaClient.getMediaInfo() : null; + /** + * Retrieves the playback state from {@code remoteMediaClient} and maps it into a {@link Player} + * state + */ + private static int fetchPlaybackState(RemoteMediaClient remoteMediaClient) { + int receiverAppStatus = remoteMediaClient.getPlayerState(); + switch (receiverAppStatus) { + case MediaStatus.PLAYER_STATE_BUFFERING: + return STATE_BUFFERING; + case MediaStatus.PLAYER_STATE_PLAYING: + case MediaStatus.PLAYER_STATE_PAUSED: + return STATE_READY; + case MediaStatus.PLAYER_STATE_IDLE: + case MediaStatus.PLAYER_STATE_UNKNOWN: + default: + return STATE_IDLE; + } } - private void updateInternalState() { - currentTimeline = Timeline.EMPTY; - currentTrackGroups = EMPTY_TRACK_GROUP_ARRAY; - currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY; - MediaInfo mediaInfo = getMediaInfo(); - if (mediaInfo == null) { - return; + /** + * Retrieves the repeat mode from {@code remoteMediaClient} and maps it into a + * {@link Player.RepeatMode}. + */ + @RepeatMode + private static int fetchRepeatMode(RemoteMediaClient remoteMediaClient) { + MediaStatus mediaStatus = remoteMediaClient.getMediaStatus(); + if (mediaStatus == null) { + // No media session active, yet. + return REPEAT_MODE_OFF; } - long streamDurationMs = mediaInfo.getStreamDuration(); - boolean isSeekable = streamDurationMs != MediaInfo.UNKNOWN_DURATION; - currentTimeline = new SinglePeriodTimeline( - isSeekable ? C.msToUs(streamDurationMs) : C.TIME_UNSET, isSeekable); - - List tracks = mediaInfo.getMediaTracks(); - if (tracks == null) { - return; + int castRepeatMode = mediaStatus.getQueueRepeatMode(); + switch (castRepeatMode) { + case MediaStatus.REPEAT_MODE_REPEAT_SINGLE: + return REPEAT_MODE_ONE; + case MediaStatus.REPEAT_MODE_REPEAT_ALL: + case MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE: + return REPEAT_MODE_ALL; + case MediaStatus.REPEAT_MODE_REPEAT_OFF: + return REPEAT_MODE_OFF; + default: + throw new IllegalStateException(); } + } - MediaStatus mediaStatus = getMediaStatus(); - long[] activeTrackIds = mediaStatus != null ? mediaStatus.getActiveTrackIds() : null; - if (activeTrackIds == null) { - activeTrackIds = EMPTY_TRACK_ID_ARRAY; - } - - TrackGroup[] trackGroups = new TrackGroup[tracks.size()]; - TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT]; - for (int i = 0; i < tracks.size(); i++) { - MediaTrack mediaTrack = tracks.get(i); - trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack)); - - long id = mediaTrack.getId(); - int trackType = MimeTypes.getTrackType(mediaTrack.getContentType()); - int rendererIndex = getRendererIndexForTrackType(trackType); - if (isTrackActive(id, activeTrackIds) && rendererIndex != C.INDEX_UNSET - && trackSelections[rendererIndex] == null) { - trackSelections[rendererIndex] = new FixedTrackSelection(trackGroups[i], 0); - } - } - currentTrackSelection = new TrackSelectionArray(trackSelections); - currentTrackGroups = new TrackGroupArray(trackGroups); + /** + * Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If + * there is no media session, returns 0. + */ + private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) { + Integer currentItemId = mediaStatus != null + ? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null; + return currentItemId != null ? currentItemId : 0; } private static boolean isTrackActive(long id, long[] activeTrackIds) { @@ -536,6 +725,19 @@ public final class CastPlayer implements Player { : C.INDEX_UNSET; } + private static int getCastRepeatMode(@RepeatMode int repeatMode) { + switch (repeatMode) { + case REPEAT_MODE_ONE: + return MediaStatus.REPEAT_MODE_REPEAT_SINGLE; + case REPEAT_MODE_ALL: + return MediaStatus.REPEAT_MODE_REPEAT_ALL; + case REPEAT_MODE_OFF: + return MediaStatus.REPEAT_MODE_REPEAT_OFF; + default: + throw new IllegalArgumentException(); + } + } + private final class StatusListener implements RemoteMediaClient.Listener, SessionManagerListener, RemoteMediaClient.ProgressListener { @@ -550,24 +752,16 @@ public final class CastPlayer implements Player { @Override public void onStatusUpdated() { - boolean playWhenReady = getPlayWhenReady(); - int playbackState = getPlaybackState(); - for (EventListener listener : listeners) { - listener.onPlayerStateChanged(playWhenReady, playbackState); - } - } - - @Override - public void onMetadataUpdated() { updateInternalState(); - for (EventListener listener : listeners) { - listener.onTracksChanged(currentTrackGroups, currentTrackSelection); - listener.onTimelineChanged(currentTimeline, null); - } } @Override - public void onQueueStatusUpdated() {} + public void onMetadataUpdated() {} + + @Override + public void onQueueStatusUpdated() { + maybeUpdateTimelineAndNotify(); + } @Override public void onPreloadStatusUpdated() {} @@ -632,36 +826,20 @@ public final class CastPlayer implements Player { // Result callbacks hooks. - private final class RepeatModeResultCallback implements ResultCallback { - - @Override - public void onResult(MediaChannelResult result) { - int statusCode = result.getStatus().getStatusCode(); - if (statusCode == CommonStatusCodes.SUCCESS) { - int repeatMode = getRepeatMode(); - for (EventListener listener : listeners) { - listener.onRepeatModeChanged(repeatMode); - } - } else { - Log.e(TAG, "Set repeat mode failed. Error code " + statusCode + ": " - + CastUtils.getLogString(statusCode)); - } - } - - } - private final class SeekResultCallback implements ResultCallback { @Override - public void onResult(MediaChannelResult result) { + public void onResult(@NonNull MediaChannelResult result) { int statusCode = result.getStatus().getStatusCode(); - if (statusCode == CommonStatusCodes.SUCCESS) { - pendingSeekPositionMs = C.TIME_UNSET; - } else if (statusCode == CastStatusCodes.REPLACED) { + if (statusCode == CastStatusCodes.REPLACED) { // A seek was executed before this one completed. Do nothing. } else { - Log.e(TAG, "Seek failed. Error code " + statusCode + ": " - + CastUtils.getLogString(statusCode)); + pendingSeekWindowIndex = C.INDEX_UNSET; + pendingSeekPositionMs = C.TIME_UNSET; + if (statusCode != CommonStatusCodes.SUCCESS) { + Log.e(TAG, "Seek failed. Error code " + statusCode + ": " + + CastUtils.getLogString(statusCode)); + } } } diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java new file mode 100644 index 0000000000..39b57148b2 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java @@ -0,0 +1,114 @@ +/* + * 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.cast; + +import android.util.SparseIntArray; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; +import com.google.android.gms.cast.MediaInfo; +import com.google.android.gms.cast.MediaQueueItem; +import java.util.Collections; +import java.util.List; + +/** + * A {@link Timeline} for Cast media queues. + */ +/* package */ final class CastTimeline extends Timeline { + + public static final CastTimeline EMPTY_CAST_TIMELINE = + new CastTimeline(Collections.emptyList()); + + private final SparseIntArray idsToIndex; + private final int[] ids; + private final long[] durationsUs; + private final long[] defaultPositionsUs; + + public CastTimeline(List items) { + int itemCount = items.size(); + int index = 0; + idsToIndex = new SparseIntArray(itemCount); + ids = new int[itemCount]; + durationsUs = new long[itemCount]; + defaultPositionsUs = new long[itemCount]; + for (MediaQueueItem item : items) { + int itemId = item.getItemId(); + ids[index] = itemId; + idsToIndex.put(itemId, index); + durationsUs[index] = getStreamDurationUs(item.getMedia()); + defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND); + index++; + } + } + + @Override + public int getWindowCount() { + return ids.length; + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + long durationUs = durationsUs[windowIndex]; + boolean isDynamic = durationUs == C.TIME_UNSET; + return window.set(ids[windowIndex], C.TIME_UNSET, C.TIME_UNSET, !isDynamic, isDynamic, + defaultPositionsUs[windowIndex], durationUs, windowIndex, windowIndex, 0); + } + + @Override + public int getPeriodCount() { + return ids.length; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + int id = ids[periodIndex]; + return period.set(id, id, periodIndex, durationsUs[periodIndex], 0); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return uid instanceof Integer ? idsToIndex.get((int) uid, C.INDEX_UNSET) : C.INDEX_UNSET; + } + + /** + * Returns whether the timeline represents a given {@code MediaQueueItem} list. + * + * @param items The {@code MediaQueueItem} list. + * @return Whether the timeline represents {@code items}. + */ + /* package */ boolean represents(List items) { + if (ids.length != items.size()) { + return false; + } + int index = 0; + for (MediaQueueItem item : items) { + if (ids[index] != item.getItemId() + || durationsUs[index] != getStreamDurationUs(item.getMedia()) + || defaultPositionsUs[index] != (long) (item.getStartTime() * C.MICROS_PER_SECOND)) { + return false; + } + index++; + } + return true; + } + + private static long getStreamDurationUs(MediaInfo mediaInfo) { + long durationMs = mediaInfo != null ? mediaInfo.getStreamDuration() + : MediaInfo.UNKNOWN_DURATION; + return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 8614cf9c85..36c674f81a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -198,7 +198,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl /** * Returns the {@link MediaSource} at a specified index. * - * @param index A index in the range of 0 <= index <= {@link #getSize()}. + * @param index An index in the range of 0 <= index <= {@link #getSize()}. * @return The {@link MediaSource} at this index. */ public synchronized MediaSource getMediaSource(int index) { From b2c245281a59973def165e09e9961cd310f1ab8b Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 08:22:30 -0700 Subject: [PATCH 0027/1327] Parse SchemeData from urn:mpeg:dash:mp4protection:2011 element Issue: #3138 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166849631 --- .../source/dash/manifest/DashManifestParser.java | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 2e85f3a1ad..2f4724c258 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 @@ -347,6 +347,16 @@ public class DashManifestParser extends DefaultHandler byte[] data = null; UUID uuid = null; boolean requiresSecureDecoder = false; + + if ("urn:mpeg:dash:mp4protection:2011".equals(schemeIdUri)) { + String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); + if (defaultKid != null) { + UUID keyId = UUID.fromString(defaultKid); + data = PsshAtomUtil.buildPsshAtom(C.COMMON_PSSH_UUID, new UUID[] {keyId}, null); + uuid = C.COMMON_PSSH_UUID; + } + } + do { xpp.next(); if (data == null && XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") From 44dc3c3ab3ed599145fff5cf0986ee30880396fe Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 08:49:51 -0700 Subject: [PATCH 0028/1327] Make all renderers DRM aware ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166852758 --- .../ext/ffmpeg/FfmpegAudioRenderer.java | 16 ++++++--- .../ext/flac/LibflacAudioRenderer.java | 14 ++++++-- .../ext/opus/LibopusAudioRenderer.java | 13 +++++-- .../ext/vp9/LibvpxVideoRenderer.java | 8 +++-- .../android/exoplayer2/BaseRenderer.java | 24 +++++++++++++ .../exoplayer2/RendererCapabilities.java | 18 ++++++---- .../audio/MediaCodecAudioRenderer.java | 24 ++++++++++--- .../audio/SimpleDecoderAudioRenderer.java | 8 +++-- .../mediacodec/MediaCodecRenderer.java | 36 ++++--------------- .../exoplayer2/metadata/MetadataRenderer.java | 6 +++- .../android/exoplayer2/text/TextRenderer.java | 10 ++++-- .../video/MediaCodecVideoRenderer.java | 10 ++++-- 12 files changed, 123 insertions(+), 64 deletions(-) diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index 453a18476e..ed8a5b0eac 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; @@ -58,13 +59,18 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected int supportsFormatInternal(Format format) { - if (!FfmpegLibrary.isAvailable()) { + protected int supportsFormatInternal(DrmSessionManager drmSessionManager, + Format format) { + String sampleMimeType = format.sampleMimeType; + if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; + } else if (!FfmpegLibrary.supportsFormat(sampleMimeType)) { + return FORMAT_UNSUPPORTED_SUBTYPE; + } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { + return FORMAT_UNSUPPORTED_DRM; + } else { + return FORMAT_HANDLED; } - String mimeType = format.sampleMimeType; - return FfmpegLibrary.supportsFormat(mimeType) ? FORMAT_HANDLED - : MimeTypes.isAudio(mimeType) ? FORMAT_UNSUPPORTED_SUBTYPE : FORMAT_UNSUPPORTED_TYPE; } @Override diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java index 246cde9d2f..dc376d2ea4 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; @@ -46,9 +47,16 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected int supportsFormatInternal(Format format) { - return FlacLibrary.isAvailable() && MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType) - ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE; + protected int supportsFormatInternal(DrmSessionManager drmSessionManager, + Format format) { + if (!FlacLibrary.isAvailable() + || !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) { + return FORMAT_UNSUPPORTED_TYPE; + } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { + return FORMAT_UNSUPPORTED_DRM; + } else { + return FORMAT_HANDLED; + } } @Override diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index 730473ddad..e4745d0c29 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -71,9 +71,16 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected int supportsFormatInternal(Format format) { - return OpusLibrary.isAvailable() && MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType) - ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE; + protected int supportsFormatInternal(DrmSessionManager drmSessionManager, + Format format) { + if (!OpusLibrary.isAvailable() + || !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) { + return FORMAT_UNSUPPORTED_TYPE; + } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { + return FORMAT_UNSUPPORTED_DRM; + } else { + return FORMAT_HANDLED; + } } @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 a947378de5..100ca6f00f 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 @@ -194,8 +194,12 @@ public final class LibvpxVideoRenderer extends BaseRenderer { @Override public int supportsFormat(Format format) { - return VpxLibrary.isAvailable() && MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType) - ? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE; + if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) { + return FORMAT_UNSUPPORTED_TYPE; + } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { + return FORMAT_UNSUPPORTED_DRM; + } + return FORMAT_HANDLED | ADAPTIVE_SEAMLESS; } @Override 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 7f14837965..a4103787d1 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 @@ -15,7 +15,10 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; @@ -309,4 +312,25 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return readEndOfStream ? streamIsFinal : stream.isReady(); } + /** + * Returns whether {@code drmSessionManager} supports the specified {@code drmInitData}, or true + * if {@code drmInitData} is null. + * + * @param drmSessionManager The drm session manager. + * @param drmInitData {@link DrmInitData} of the format to check for support. + * @return Whether {@code drmSessionManager} supports the specified {@code drmInitData}, or + * true if {@code drmInitData} is null. + */ + protected static boolean supportsFormatDrm(@Nullable DrmSessionManager drmSessionManager, + @Nullable DrmInitData drmInitData) { + if (drmInitData == null) { + // Content is unencrypted. + return true; + } else if (drmSessionManager == null) { + // Content is encrypted, but no drm session manager is available. + return false; + } + return drmSessionManager.canAcquireSession(drmInitData); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java index 3f1be20cfb..de0d481386 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java @@ -34,7 +34,9 @@ public interface RendererCapabilities { int FORMAT_HANDLED = 0b100; /** * The {@link Renderer} is capable of rendering formats with the same mime type, but the - * properties of the format exceed the renderer's capability. + * properties of the format exceed the renderer's capabilities. There is a chance the renderer + * will be able to play the format in practice because some renderers report their capabilities + * conservatively, but the expected outcome is that playback will fail. *

      * Example: The {@link Renderer} is capable of rendering H264 and the format's mime type is * {@link MimeTypes#VIDEO_H264}, but the format's resolution exceeds the maximum limit supported @@ -42,12 +44,12 @@ public interface RendererCapabilities { */ int FORMAT_EXCEEDS_CAPABILITIES = 0b011; /** - * The {@link Renderer} is capable of rendering formats with the same mime type, but the - * drm scheme used is not supported. + * The {@link Renderer} is capable of rendering formats with the same mime type, but is not + * capable of rendering the format because the format's drm protection is not supported. *

      * Example: The {@link Renderer} is capable of rendering H264 and the format's mime type is - * {@link MimeTypes#VIDEO_H264}, but the format indicates cbcs encryption, which is not supported - * by the underlying content decryption module. + * {@link MimeTypes#VIDEO_H264}, but the format indicates PlayReady drm protection where-as the + * renderer only supports Widevine. */ int FORMAT_UNSUPPORTED_DRM = 0b010; /** @@ -121,9 +123,11 @@ public interface RendererCapabilities { * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. *

    • The level of support for adapting from the format to another format of the same mime type. * One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and - * {@link #ADAPTIVE_NOT_SUPPORTED}.
    • + * {@link #ADAPTIVE_NOT_SUPPORTED}. Only set if the level of support for the format itself is + * {@link #FORMAT_HANDLED} or {@link #FORMAT_EXCEEDS_CAPABILITIES}. *
    • The level of support for tunneling. One of {@link #TUNNELING_SUPPORTED} and - * {@link #TUNNELING_NOT_SUPPORTED}.
    • + * {@link #TUNNELING_NOT_SUPPORTED}. Only set if the level of support for the format itself is + * {@link #FORMAT_HANDLED} or {@link #FORMAT_EXCEEDS_CAPABILITIES}. *
    * The individual properties can be retrieved by performing a bitwise AND with * {@link #FORMAT_SUPPORT_MASK}, {@link #ADAPTIVE_SUPPORT_MASK} and 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 e146238dcc..7f157e5866 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 @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; +import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; @@ -138,19 +139,34 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) + protected int supportsFormat(MediaCodecSelector mediaCodecSelector, + DrmSessionManager drmSessionManager, Format format) throws DecoderQueryException { String mimeType = format.sampleMimeType; if (!MimeTypes.isAudio(mimeType)) { return FORMAT_UNSUPPORTED_TYPE; } int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; - if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) { + boolean supportsFormatDrm = supportsFormatDrm(drmSessionManager, format.drmInitData); + if (supportsFormatDrm && allowPassthrough(mimeType) + && mediaCodecSelector.getPassthroughDecoderInfo() != null) { return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED; } - MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false); + boolean requiresSecureDecryption = false; + DrmInitData drmInitData = format.drmInitData; + if (drmInitData != null) { + for (int i = 0; i < drmInitData.schemeDataCount; i++) { + requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption; + } + } + MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, + requiresSecureDecryption); if (decoderInfo == null) { - return FORMAT_UNSUPPORTED_SUBTYPE; + return requiresSecureDecryption && mediaCodecSelector.getDecoderInfo(mimeType, false) != null + ? FORMAT_UNSUPPORTED_DRM : FORMAT_UNSUPPORTED_SUBTYPE; + } + if (!supportsFormatDrm) { + return FORMAT_UNSUPPORTED_DRM; } // Note: We assume support for unknown sampleRate and channelCount. boolean decoderCapable = Util.SDK_INT < 21 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 c4a55eeb02..012f06da39 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 @@ -159,8 +159,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override public final int supportsFormat(Format format) { - int formatSupport = supportsFormatInternal(format); - if (formatSupport == FORMAT_UNSUPPORTED_TYPE || formatSupport == FORMAT_UNSUPPORTED_SUBTYPE) { + int formatSupport = supportsFormatInternal(drmSessionManager, format); + if (formatSupport <= FORMAT_UNSUPPORTED_DRM) { return formatSupport; } int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; @@ -171,10 +171,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for * {@link #supportsFormat(Format)}. * + * @param drmSessionManager The renderer's {@link DrmSessionManager}. * @param format The format. * @return The extent to which the renderer supports the format itself. */ - protected abstract int supportsFormatInternal(Format format); + protected abstract int supportsFormatInternal(DrmSessionManager drmSessionManager, + Format format); @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { 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 31c6a824ef..d6725e373a 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,7 +32,6 @@ import com.google.android.exoplayer2.Format; 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.DrmInitData; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -169,7 +168,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private static final int ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT = 32; private final MediaCodecSelector mediaCodecSelector; - @Nullable private final DrmSessionManager drmSessionManager; + @Nullable + private final DrmSessionManager drmSessionManager; private final boolean playClearSamplesWithoutKeys; private final DecoderInputBuffer buffer; private final DecoderInputBuffer flagsOnlyBuffer; @@ -247,14 +247,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override public final int supportsFormat(Format format) throws ExoPlaybackException { try { - int formatSupport = supportsFormat(mediaCodecSelector, format); - if ((formatSupport & FORMAT_SUPPORT_MASK) > FORMAT_UNSUPPORTED_DRM - && !isDrmSchemeSupported(drmSessionManager, format.drmInitData)) { - // The renderer advertises higher support than FORMAT_UNSUPPORTED_DRM but the DRM scheme is - // not supported. The format support is truncated to reflect this. - formatSupport = (formatSupport & ~FORMAT_SUPPORT_MASK) | FORMAT_UNSUPPORTED_DRM; - } - return formatSupport; + return supportsFormat(mediaCodecSelector, drmSessionManager, format); } catch (DecoderQueryException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } @@ -264,12 +257,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * Returns the extent to which the renderer is capable of supporting a given format. * * @param mediaCodecSelector The decoder selector. + * @param drmSessionManager The renderer's {@link DrmSessionManager}. * @param format The format. * @return The extent to which the renderer is capable of supporting the given format. See * {@link #supportsFormat(Format)} for more detail. * @throws DecoderQueryException If there was an error querying decoders. */ - protected abstract int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) + protected abstract int supportsFormat(MediaCodecSelector mediaCodecSelector, + DrmSessionManager drmSessionManager, Format format) throws DecoderQueryException; /** @@ -1083,25 +1078,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } - /** - * Returns whether the encryption scheme is supported, or true if {@code drmInitData} is null. - * - * @param drmSessionManager The drm session manager associated with the renderer. - * @param drmInitData {@link DrmInitData} of the format to check for support. - * @return Whether the encryption scheme is supported, or true if {@code drmInitData} is null. - */ - private static boolean isDrmSchemeSupported(@Nullable DrmSessionManager drmSessionManager, - @Nullable DrmInitData drmInitData) { - if (drmInitData == null) { - // Content is unencrypted. - return true; - } else if (drmSessionManager == null) { - // Content is encrypted, but no drm session manager is available. - return false; - } - return drmSessionManager.canAcquireSession(drmInitData); - } - /** * Returns whether the decoder is known to fail when flushed. *

    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 f46dd467c8..869e9306a5 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 @@ -92,7 +92,11 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { @Override public int supportsFormat(Format format) { - return decoderFactory.supportsFormat(format) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE; + if (decoderFactory.supportsFormat(format)) { + return supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM; + } else { + return FORMAT_UNSUPPORTED_TYPE; + } } @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 8e1966305e..c3dc2383ef 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 @@ -117,9 +117,13 @@ public final class TextRenderer extends BaseRenderer implements Callback { @Override public int supportsFormat(Format format) { - return decoderFactory.supportsFormat(format) ? FORMAT_HANDLED - : (MimeTypes.isText(format.sampleMimeType) ? FORMAT_UNSUPPORTED_SUBTYPE - : FORMAT_UNSUPPORTED_TYPE); + if (decoderFactory.supportsFormat(format)) { + return supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM; + } else if (MimeTypes.isText(format.sampleMimeType)) { + return FORMAT_UNSUPPORTED_SUBTYPE; + } else { + return FORMAT_UNSUPPORTED_TYPE; + } } @Override 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 c11c415cd7..f70d74e413 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 @@ -186,7 +186,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) + protected int supportsFormat(MediaCodecSelector mediaCodecSelector, + DrmSessionManager drmSessionManager, Format format) throws DecoderQueryException { String mimeType = format.sampleMimeType; if (!MimeTypes.isVideo(mimeType)) { @@ -202,9 +203,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, requiresSecureDecryption); if (decoderInfo == null) { - return FORMAT_UNSUPPORTED_SUBTYPE; + return requiresSecureDecryption && mediaCodecSelector.getDecoderInfo(mimeType, false) != null + ? FORMAT_UNSUPPORTED_DRM : FORMAT_UNSUPPORTED_SUBTYPE; + } + if (!supportsFormatDrm(drmSessionManager, drmInitData)) { + return FORMAT_UNSUPPORTED_DRM; } - boolean decoderCapable = decoderInfo.isCodecSupported(format.codecs); if (decoderCapable && format.width > 0 && format.height > 0) { if (Util.SDK_INT >= 21) { From 55e928f75acb78107b1defb702e08aa6f3aa42e5 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 08:50:17 -0700 Subject: [PATCH 0029/1327] Make LeanbackPlayerAdapter use a ControlDispatcher + Misc cleanup 1. Make LeanbackPlayerAdapter use a ControlDispatcher. This allows apps to suppress control events in some circumstances, and is in-line with our mobile controls. 2. Misc simplifications and cleanup to LeanbackPlayerAdapter. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166852816 --- .../ext/leanback/LeanbackPlayerAdapter.java | 126 ++++++++++-------- .../android/exoplayer2/ControlDispatcher.java | 67 ++++++++++ .../exoplayer2/DefaultControlDispatcher.java | 50 +++++++ .../exoplayer2/ui/PlaybackControlView.java | 107 +++------------ .../exoplayer2/ui/SimpleExoPlayerView.java | 7 +- 5 files changed, 215 insertions(+), 142 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index 8a207bea8f..f5ef8b2ca4 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.leanback; import android.content.Context; import android.os.Handler; +import android.support.annotation.Nullable; import android.support.v17.leanback.R; import android.support.v17.leanback.media.PlaybackGlueHost; import android.support.v17.leanback.media.PlayerAdapter; @@ -25,6 +26,8 @@ import android.util.Pair; import android.view.Surface; import android.view.SurfaceHolder; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ControlDispatcher; +import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.PlaybackParameters; @@ -48,13 +51,13 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { private final SimpleExoPlayer player; private final Handler handler; private final ComponentListener componentListener; - private final Runnable updatePlayerRunnable; + private final Runnable updateProgressRunnable; + private ControlDispatcher controlDispatcher; private ErrorMessageProvider errorMessageProvider; private SurfaceHolderGlueHost surfaceHolderGlueHost; - private boolean initialized; private boolean hasSurface; - private boolean isBuffering; + private boolean lastNotifiedPreparedState; /** * Builds an instance. Note that the {@code PlayerAdapter} does not manage the lifecycle of the @@ -70,7 +73,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { this.player = player; handler = new Handler(); componentListener = new ComponentListener(); - updatePlayerRunnable = new Runnable() { + controlDispatcher = new DefaultControlDispatcher(); + updateProgressRunnable = new Runnable() { @Override public void run() { Callback callback = getCallback(); @@ -81,34 +85,15 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { }; } - @Override - public void onAttachedToHost(PlaybackGlueHost host) { - if (host instanceof SurfaceHolderGlueHost) { - surfaceHolderGlueHost = ((SurfaceHolderGlueHost) host); - surfaceHolderGlueHost.setSurfaceHolderCallback(componentListener); - } - notifyListeners(); - player.addListener(componentListener); - player.addVideoListener(componentListener); - } - - private void notifyListeners() { - boolean oldIsPrepared = isPrepared(); - int playbackState = player.getPlaybackState(); - boolean isInitialized = playbackState != Player.STATE_IDLE; - isBuffering = playbackState == Player.STATE_BUFFERING; - boolean hasEnded = playbackState == Player.STATE_ENDED; - - initialized = isInitialized; - Callback callback = getCallback(); - if (oldIsPrepared != isPrepared()) { - callback.onPreparedStateChanged(this); - } - callback.onPlayStateChanged(this); - callback.onBufferingStateChanged(this, isBuffering || !initialized); - if (hasEnded) { - callback.onPlayCompleted(this); - } + /** + * Sets the {@link ControlDispatcher}. + * + * @param controlDispatcher The {@link ControlDispatcher}, or null to use + * {@link DefaultControlDispatcher}. + */ + public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) { + this.controlDispatcher = controlDispatcher == null ? new DefaultControlDispatcher() + : controlDispatcher; } /** @@ -121,6 +106,19 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { this.errorMessageProvider = errorMessageProvider; } + // PlayerAdapter implementation. + + @Override + public void onAttachedToHost(PlaybackGlueHost host) { + if (host instanceof SurfaceHolderGlueHost) { + surfaceHolderGlueHost = ((SurfaceHolderGlueHost) host); + surfaceHolderGlueHost.setSurfaceHolderCallback(componentListener); + } + notifyStateChanged(); + player.addListener(componentListener); + player.addVideoListener(componentListener); + } + @Override public void onDetachedFromHost() { player.removeListener(componentListener); @@ -129,56 +127,59 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { surfaceHolderGlueHost.setSurfaceHolderCallback(null); surfaceHolderGlueHost = null; } - initialized = false; hasSurface = false; Callback callback = getCallback(); callback.onBufferingStateChanged(this, false); callback.onPlayStateChanged(this); - callback.onPreparedStateChanged(this); + maybeNotifyPreparedStateChanged(callback); } @Override - public void setProgressUpdatingEnabled(final boolean enabled) { - handler.removeCallbacks(updatePlayerRunnable); + public void setProgressUpdatingEnabled(boolean enabled) { + handler.removeCallbacks(updateProgressRunnable); if (enabled) { - handler.post(updatePlayerRunnable); + handler.post(updateProgressRunnable); } } @Override public boolean isPlaying() { - return initialized && player.getPlayWhenReady(); + int playbackState = player.getPlaybackState(); + return playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED + && player.getPlayWhenReady(); } @Override public long getDuration() { long durationMs = player.getDuration(); - return durationMs != C.TIME_UNSET ? durationMs : -1; + return durationMs == C.TIME_UNSET ? -1 : durationMs; } @Override public long getCurrentPosition() { - return initialized ? player.getCurrentPosition() : -1; + return player.getPlaybackState() == Player.STATE_IDLE ? -1 : player.getCurrentPosition(); } @Override public void play() { if (player.getPlaybackState() == Player.STATE_ENDED) { - player.seekToDefaultPosition(); + controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); + } + if (controlDispatcher.dispatchSetPlayWhenReady(player, true)) { + getCallback().onPlayStateChanged(this); } - player.setPlayWhenReady(true); - getCallback().onPlayStateChanged(this); } @Override public void pause() { - player.setPlayWhenReady(false); - getCallback().onPlayStateChanged(this); + if (controlDispatcher.dispatchSetPlayWhenReady(player, false)) { + getCallback().onPlayStateChanged(this); + } } @Override public void seekTo(long positionMs) { - player.seekTo(positionMs); + controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), positionMs); } @Override @@ -188,13 +189,35 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { @Override public boolean isPrepared() { - return initialized && (surfaceHolderGlueHost == null || hasSurface); + return player.getPlaybackState() != Player.STATE_IDLE + && (surfaceHolderGlueHost == null || hasSurface); } - private void setVideoSurface(Surface surface) { + // Internal methods. + + /* package */ void setVideoSurface(Surface surface) { hasSurface = surface != null; player.setVideoSurface(surface); - getCallback().onPreparedStateChanged(this); + maybeNotifyPreparedStateChanged(getCallback()); + } + + /* package */ void notifyStateChanged() { + int playbackState = player.getPlaybackState(); + Callback callback = getCallback(); + maybeNotifyPreparedStateChanged(callback); + callback.onPlayStateChanged(this); + callback.onBufferingStateChanged(this, playbackState == Player.STATE_BUFFERING); + if (playbackState == Player.STATE_ENDED) { + callback.onPlayCompleted(this); + } + } + + private void maybeNotifyPreparedStateChanged(Callback callback) { + boolean isPrepared = isPrepared(); + if (lastNotifiedPreparedState != isPrepared) { + lastNotifiedPreparedState = isPrepared; + callback.onPreparedStateChanged(this); + } } private final class ComponentListener implements Player.EventListener, @@ -208,7 +231,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } @Override - public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) { + public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) { // Do nothing. } @@ -221,7 +244,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - notifyListeners(); + notifyStateChanged(); } @Override @@ -292,4 +315,3 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } } - diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java new file mode 100644 index 0000000000..21c596e6d4 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.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; + +import com.google.android.exoplayer2.Player.RepeatMode; + +/** + * Dispatches operations to the {@link Player}. + *

    + * Implementations may choose to suppress (e.g. prevent playback from resuming if audio focus is + * denied) or modify (e.g. change the seek position to prevent a user from seeking past a + * non-skippable advert) operations. + */ +public interface ControlDispatcher { + + /** + * Dispatches a {@link Player#setPlayWhenReady(boolean)} operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @param playWhenReady Whether playback should proceed when ready. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady); + + /** + * Dispatches a {@link Player#seekTo(int, long)} operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @param windowIndex The index of the window. + * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to + * the window's default position. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchSeekTo(Player player, int windowIndex, long positionMs); + + /** + * Dispatches a {@link Player#setRepeatMode(int)} operation. + * + * @param player The {@link 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(Player player, @RepeatMode int repeatMode); + + /** + * Dispatches a {@link Player#setShuffleModeEnabled(boolean)} operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @param shuffleModeEnabled Whether shuffling is enabled. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled); + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java new file mode 100644 index 0000000000..84711d752a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java @@ -0,0 +1,50 @@ +/* + * 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.Player.RepeatMode; + +/** + * Default {@link ControlDispatcher} that dispatches all operations to the player without + * modification. + */ +public class DefaultControlDispatcher implements ControlDispatcher { + + @Override + public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) { + player.setPlayWhenReady(playWhenReady); + return true; + } + + @Override + public boolean dispatchSeekTo(Player player, int windowIndex, long positionMs) { + player.seekTo(windowIndex, positionMs); + return true; + } + + @Override + public boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode) { + player.setRepeatMode(repeatMode); + return true; + } + + @Override + public boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled) { + player.setShuffleModeEnabled(shuffleModeEnabled); + return true; + } + +} 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 f83bab5770..c89feaebf5 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 @@ -34,7 +34,6 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -180,6 +179,12 @@ public class PlaybackControlView extends FrameLayout { ExoPlayerLibraryInfo.registerModule("goog.exo.ui"); } + /** + * @deprecated Use {@link com.google.android.exoplayer2.ControlDispatcher}. + */ + @Deprecated + public interface ControlDispatcher extends com.google.android.exoplayer2.ControlDispatcher {} + /** * Listener to be notified about changes of the visibility of the UI control. */ @@ -194,86 +199,13 @@ public class PlaybackControlView extends FrameLayout { } + private static final class DefaultControlDispatcher + extends com.google.android.exoplayer2.DefaultControlDispatcher implements ControlDispatcher {} /** - * Dispatches operations to the {@link Player}. - *

    - * Implementations may choose to suppress (e.g. prevent playback from resuming if audio focus is - * denied) or modify (e.g. change the seek position to prevent a user from seeking past a - * non-skippable advert) operations. + * @deprecated Use {@link com.google.android.exoplayer2.DefaultControlDispatcher}. */ - public interface ControlDispatcher { - - /** - * Dispatches a {@link Player#setPlayWhenReady(boolean)} operation. - * - * @param player The {@link Player} to which the operation should be dispatched. - * @param playWhenReady Whether playback should proceed when ready. - * @return True if the operation was dispatched. False if suppressed. - */ - boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady); - - /** - * Dispatches a {@link Player#seekTo(int, long)} operation. - * - * @param player The {@link Player} to which the operation should be dispatched. - * @param windowIndex The index of the window. - * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek - * to the window's default position. - * @return True if the operation was dispatched. False if suppressed. - */ - boolean dispatchSeekTo(Player player, int windowIndex, long positionMs); - - /** - * Dispatches a {@link Player#setRepeatMode(int)} operation. - * - * @param player The {@link 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(Player player, @RepeatMode int repeatMode); - - /** - * Dispatches a {@link Player#setShuffleModeEnabled(boolean)} operation. - * - * @param player The {@link Player} to which the operation should be dispatched. - * @param shuffleModeEnabled Whether shuffling is enabled. - * @return True if the operation was dispatched. False if suppressed. - */ - boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled); - - } - - /** - * Default {@link ControlDispatcher} that dispatches operations to the player without - * modification. - */ - public static final ControlDispatcher DEFAULT_CONTROL_DISPATCHER = new ControlDispatcher() { - - @Override - public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) { - player.setPlayWhenReady(playWhenReady); - return true; - } - - @Override - public boolean dispatchSeekTo(Player player, int windowIndex, long positionMs) { - player.seekTo(windowIndex, positionMs); - return true; - } - - @Override - public boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode) { - player.setRepeatMode(repeatMode); - return true; - } - - @Override - public boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled) { - player.setShuffleModeEnabled(shuffleModeEnabled); - return true; - } - - }; + @Deprecated + public static final ControlDispatcher DEFAULT_CONTROL_DISPATCHER = new DefaultControlDispatcher(); /** * The default fast forward increment, in milliseconds. @@ -325,7 +257,7 @@ public class PlaybackControlView extends FrameLayout { private final String repeatAllButtonContentDescription; private Player player; - private ControlDispatcher controlDispatcher; + private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; private VisibilityListener visibilityListener; private boolean isAttachedToWindow; @@ -400,7 +332,7 @@ public class PlaybackControlView extends FrameLayout { extraAdGroupTimesMs = new long[0]; extraPlayedAdGroups = new boolean[0]; componentListener = new ComponentListener(); - controlDispatcher = DEFAULT_CONTROL_DISPATCHER; + controlDispatcher = new com.google.android.exoplayer2.DefaultControlDispatcher(); LayoutInflater.from(context).inflate(controllerLayoutId, this); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); @@ -534,14 +466,15 @@ public class PlaybackControlView extends FrameLayout { } /** - * Sets the {@link ControlDispatcher}. + * Sets the {@link com.google.android.exoplayer2.ControlDispatcher}. * - * @param controlDispatcher The {@link ControlDispatcher}, or null to use - * {@link #DEFAULT_CONTROL_DISPATCHER}. + * @param controlDispatcher The {@link com.google.android.exoplayer2.ControlDispatcher}, or null + * to use {@link com.google.android.exoplayer2.DefaultControlDispatcher}. */ - public void setControlDispatcher(ControlDispatcher controlDispatcher) { - this.controlDispatcher = controlDispatcher == null ? DEFAULT_CONTROL_DISPATCHER - : controlDispatcher; + public void setControlDispatcher( + @Nullable com.google.android.exoplayer2.ControlDispatcher controlDispatcher) { + this.controlDispatcher = controlDispatcher == null + ? new com.google.android.exoplayer2.DefaultControlDispatcher() : controlDispatcher; } /** 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 a8926f9ecc..5b6e11c5e4 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 @@ -34,6 +34,8 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ControlDispatcher; +import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; @@ -47,7 +49,6 @@ import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode; -import com.google.android.exoplayer2.ui.PlaybackControlView.ControlDispatcher; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.RepeatModeUtil; import com.google.android.exoplayer2.util.Util; @@ -616,9 +617,9 @@ public final class SimpleExoPlayerView extends FrameLayout { * Sets the {@link ControlDispatcher}. * * @param controlDispatcher The {@link ControlDispatcher}, or null to use - * {@link PlaybackControlView#DEFAULT_CONTROL_DISPATCHER}. + * {@link DefaultControlDispatcher}. */ - public void setControlDispatcher(ControlDispatcher controlDispatcher) { + public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) { Assertions.checkState(controller != null); controller.setControlDispatcher(controlDispatcher); } From d9cd13ce746c496adb6c327cb749e74afcfe2f05 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 12:15:50 -0700 Subject: [PATCH 0030/1327] Automated rollback of changelist 166843123. *** Reason for rollback *** Doesn't work because trackOutputProvider can be null when extracting init data. *** Original change description *** Don't copy primary-track format to non-primary tracks Copying non-primary-track formats to non-primary tracks looks non-trivial (I tried; went down a dead-end), so leaving that for now. *** ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166883654 --- .../source/chunk/BaseMediaChunkOutput.java | 27 ++++--------------- .../source/chunk/ChunkExtractorWrapper.java | 24 +++++------------ .../source/chunk/ChunkSampleStream.java | 10 +++++-- 3 files changed, 20 insertions(+), 41 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java index 0b6c196d7c..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 @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.source.chunk; -import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -33,23 +32,12 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut private final SampleQueue[] sampleQueues; /** - * @param primaryTrackType The type of the primary track. - * @param primarySampleQueue The primary track sample queues. - * @param embeddedTrackTypes The types of any embedded tracks, or null. - * @param embeddedSampleQueues The track sample queues for any embedded tracks, or null. + * @param trackTypes The track types of the individual track outputs. + * @param sampleQueues The individual sample queues. */ - @SuppressWarnings("ConstantConditions") - public BaseMediaChunkOutput(int primaryTrackType, SampleQueue primarySampleQueue, - @Nullable int[] embeddedTrackTypes, @Nullable SampleQueue[] embeddedSampleQueues) { - int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; - trackTypes = new int[1 + embeddedTrackCount]; - sampleQueues = new SampleQueue[1 + embeddedTrackCount]; - trackTypes[0] = primaryTrackType; - sampleQueues[0] = primarySampleQueue; - for (int i = 0; i < embeddedTrackCount; i++) { - trackTypes[i + 1] = embeddedTrackTypes[i]; - sampleQueues[i + 1] = embeddedSampleQueues[i]; - } + public BaseMediaChunkOutput(int[] trackTypes, SampleQueue[] sampleQueues) { + this.trackTypes = trackTypes; + this.sampleQueues = sampleQueues; } @Override @@ -63,11 +51,6 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut return new DummyTrackOutput(); } - @Override - public boolean isPrimaryTrack(int type) { - return type == trackTypes[0]; - } - /** * Returns the current absolute write indices of the individual sample queues. */ 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 eda9ed3cf7..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 @@ -45,22 +45,13 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { *

    * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}. * - * @param id The track identifier. - * @param type The track type. Typically one of the {@link com.google.android.exoplayer2.C} - * {@code TRACK_TYPE_*} constants. + * @param id A track identifier. + * @param type The type of the track. Typically one of the + * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. * @return The {@link TrackOutput} for the given track identifier. */ TrackOutput track(int id, int type); - /** - * Returns whether the specified type corresponds to the primary track. - * - * @param type The track type. Typically one of the {@link com.google.android.exoplayer2.C} - * {@code TRACK_TYPE_*} constants. - * @return Whether {@code type} corresponds to the primary track. - */ - boolean isPrimaryTrack(int type); - } public final Extractor extractor; @@ -155,7 +146,6 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { private final Format manifestFormat; public Format sampleFormat; - private boolean isPrimaryTrack; private TrackOutput trackOutput; public BindingTrackOutput(int id, int type, Format manifestFormat) { @@ -169,17 +159,17 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { trackOutput = new DummyTrackOutput(); return; } - isPrimaryTrack = trackOutputProvider.isPrimaryTrack(type); trackOutput = trackOutputProvider.track(id, type); - if (sampleFormat != null) { + if (trackOutput != null) { trackOutput.format(sampleFormat); } } @Override public void format(Format format) { - // TODO: Non-primary tracks should be copied with data from their own manifest formats. - sampleFormat = isPrimaryTrack ? format.copyWithManifestFormatInfo(manifestFormat) : format; + // TODO: This should only happen for the primary track. Additional metadata/text tracks need + // to be copied with different manifest derived formats. + sampleFormat = format.copyWithManifestFormatInfo(manifestFormat); trackOutput.format(sampleFormat); } 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 e8586f7230..f2609a0ffd 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 @@ -87,15 +87,21 @@ public class ChunkSampleStream implements SampleStream, S int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; embeddedSampleQueues = new SampleQueue[embeddedTrackCount]; embeddedTracksSelected = new boolean[embeddedTrackCount]; + int[] trackTypes = new int[1 + embeddedTrackCount]; + SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount]; primarySampleQueue = new SampleQueue(allocator); + trackTypes[0] = primaryTrackType; + sampleQueues[0] = primarySampleQueue; + for (int i = 0; i < embeddedTrackCount; i++) { SampleQueue sampleQueue = new SampleQueue(allocator); embeddedSampleQueues[i] = sampleQueue; + sampleQueues[i + 1] = sampleQueue; + trackTypes[i + 1] = embeddedTrackTypes[i]; } - mediaChunkOutput = new BaseMediaChunkOutput(primaryTrackType, primarySampleQueue, - embeddedTrackTypes, embeddedSampleQueues); + mediaChunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues); pendingResetPositionUs = positionUs; lastSeekPositionUs = positionUs; } From 5bed2bf5032a4e5bea36a30a667a795ed172798a Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 13:55:19 -0700 Subject: [PATCH 0031/1327] Don't copy primary-track format to non-primary tracks This time plumbing the track type in from the other side. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166898172 --- .../source/chunk/ChunkExtractorWrapper.java | 35 +++++++++------- .../exoplayer2/source/dash/DashUtil.java | 41 +++++++++++-------- .../source/dash/DefaultDashChunkSource.java | 10 ++--- .../smoothstreaming/DefaultSsChunkSource.java | 2 +- 4 files changed, 51 insertions(+), 37 deletions(-) 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 07d1cce8cb..17eb30dee9 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 @@ -29,9 +29,10 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; /** - * An {@link Extractor} wrapper for loading chunks containing a single track. + * An {@link Extractor} wrapper for loading chunks that contain a single primary track, and possibly + * additional embedded tracks. *

    - * The wrapper allows switching of the {@link TrackOutput} that receives parsed data. + * The wrapper allows switching of the {@link TrackOutput}s that receive parsed data. */ public final class ChunkExtractorWrapper implements ExtractorOutput { @@ -56,7 +57,8 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { public final Extractor extractor; - private final Format manifestFormat; + private final int primaryTrackType; + private final Format primaryTrackManifestFormat; private final SparseArray bindingTrackOutputs; private boolean extractorInitialized; @@ -66,12 +68,16 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { /** * @param extractor The extractor to wrap. - * @param manifestFormat A manifest defined {@link Format} whose data should be merged into any - * sample {@link Format} output from the {@link Extractor}. + * @param primaryTrackType The type of the primary track. Typically one of the + * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. + * @param primaryTrackManifestFormat A manifest defined {@link Format} whose data should be merged + * into any sample {@link Format} output from the {@link Extractor} for the primary track. */ - public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat) { + public ChunkExtractorWrapper(Extractor extractor, int primaryTrackType, + Format primaryTrackManifestFormat) { this.extractor = extractor; - this.manifestFormat = manifestFormat; + this.primaryTrackType = primaryTrackType; + this.primaryTrackManifestFormat = primaryTrackManifestFormat; bindingTrackOutputs = new SparseArray<>(); } @@ -90,8 +96,8 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { } /** - * Initializes the extractor to output to the provided {@link TrackOutput}, and configures it to - * receive data from a new chunk. + * Initializes the wrapper to output to {@link TrackOutput}s provided by the specified + * {@link TrackOutputProvider}, and configures the extractor to receive data from a new chunk. * * @param trackOutputProvider The provider of {@link TrackOutput}s that will receive sample data. */ @@ -116,7 +122,9 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { if (bindingTrackOutput == null) { // Assert that if we're seeing a new track we have not seen endTracks. Assertions.checkState(sampleFormats == null); - bindingTrackOutput = new BindingTrackOutput(id, type, manifestFormat); + // TODO: Manifest formats for embedded tracks should also be passed here. + bindingTrackOutput = new BindingTrackOutput(id, type, + type == primaryTrackType ? primaryTrackManifestFormat : null); bindingTrackOutput.bind(trackOutputProvider); bindingTrackOutputs.put(id, bindingTrackOutput); } @@ -160,16 +168,15 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { return; } trackOutput = trackOutputProvider.track(id, type); - if (trackOutput != null) { + if (sampleFormat != null) { trackOutput.format(sampleFormat); } } @Override public void format(Format format) { - // TODO: This should only happen for the primary track. Additional metadata/text tracks need - // to be copied with different manifest derived formats. - sampleFormat = format.copyWithManifestFormatInfo(manifestFormat); + sampleFormat = manifestFormat != null ? format.copyWithManifestFormatInfo(manifestFormat) + : format; trackOutput.format(sampleFormat); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java index 59fbfb18fe..ed2f916b87 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java @@ -72,9 +72,11 @@ public final class DashUtil { */ public static DrmInitData loadDrmInitData(DataSource dataSource, Period period) throws IOException, InterruptedException { - Representation representation = getFirstRepresentation(period, C.TRACK_TYPE_VIDEO); + int primaryTrackType = C.TRACK_TYPE_VIDEO; + Representation representation = getFirstRepresentation(period, primaryTrackType); if (representation == null) { - representation = getFirstRepresentation(period, C.TRACK_TYPE_AUDIO); + primaryTrackType = C.TRACK_TYPE_AUDIO; + representation = getFirstRepresentation(period, primaryTrackType); if (representation == null) { return null; } @@ -85,7 +87,7 @@ public final class DashUtil { // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. return drmInitData; } - Format sampleFormat = DashUtil.loadSampleFormat(dataSource, representation); + Format sampleFormat = DashUtil.loadSampleFormat(dataSource, primaryTrackType, representation); return sampleFormat == null ? null : sampleFormat.drmInitData; } @@ -93,15 +95,17 @@ public final class DashUtil { * Loads initialization data for the {@code representation} and returns the sample {@link Format}. * * @param dataSource The source from which the data should be loaded. + * @param trackType The type of the representation. Typically one of the + * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. * @param representation The representation which initialization chunk belongs to. * @return the sample {@link Format} of the given representation. * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static Format loadSampleFormat(DataSource dataSource, Representation representation) - throws IOException, InterruptedException { - ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, representation, - false); + public static Format loadSampleFormat(DataSource dataSource, int trackType, + Representation representation) throws IOException, InterruptedException { + ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, trackType, + representation, false); return extractorWrapper == null ? null : extractorWrapper.getSampleFormats()[0]; } @@ -110,16 +114,18 @@ public final class DashUtil { * ChunkIndex}. * * @param dataSource The source from which the data should be loaded. + * @param trackType The type of the representation. Typically one of the + * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. * @param representation The representation which initialization chunk belongs to. * @return The {@link ChunkIndex} of the given representation, or null if no initialization or * index data exists. * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static ChunkIndex loadChunkIndex(DataSource dataSource, Representation representation) - throws IOException, InterruptedException { - ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, representation, - true); + public static ChunkIndex loadChunkIndex(DataSource dataSource, int trackType, + Representation representation) throws IOException, InterruptedException { + ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, trackType, + representation, true); return extractorWrapper == null ? null : (ChunkIndex) extractorWrapper.getSeekMap(); } @@ -128,6 +134,8 @@ public final class DashUtil { * returns a {@link ChunkExtractorWrapper} which contains the output. * * @param dataSource The source from which the data should be loaded. + * @param trackType The type of the representation. Typically one of the + * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. * @param representation The representation which initialization chunk belongs to. * @param loadIndex Whether to load index data too. * @return A {@link ChunkExtractorWrapper} for the {@code representation}, or null if no @@ -135,14 +143,13 @@ public final class DashUtil { * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - private static ChunkExtractorWrapper loadInitializationData(DataSource dataSource, - Representation representation, boolean loadIndex) - throws IOException, InterruptedException { + private static ChunkExtractorWrapper loadInitializationData(DataSource dataSource, int trackType, + Representation representation, boolean loadIndex) throws IOException, InterruptedException { RangedUri initializationUri = representation.getInitializationUri(); if (initializationUri == null) { return null; } - ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(representation.format); + ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(trackType, representation.format); RangedUri requestUri; if (loadIndex) { RangedUri indexUri = representation.getIndexUri(); @@ -174,12 +181,12 @@ public final class DashUtil { initializationChunk.load(); } - private static ChunkExtractorWrapper newWrappedExtractor(Format format) { + private static ChunkExtractorWrapper newWrappedExtractor(int trackType, Format format) { String mimeType = format.containerMimeType; boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor(); - return new ChunkExtractorWrapper(extractor, format); + return new ChunkExtractorWrapper(extractor, trackType, format); } private static Representation getFirstRepresentation(Period period, int type) { 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 dd62d47621..c6c1461001 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 @@ -134,8 +134,8 @@ public class DefaultDashChunkSource implements DashChunkSource { 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); + representationHolders[i] = new RepresentationHolder(periodDurationUs, trackType, + representation, enableEventMessageTrack, enableCea608Track); } } @@ -390,8 +390,8 @@ public class DefaultDashChunkSource implements DashChunkSource { private long periodDurationUs; private int segmentNumShift; - /* package */ RepresentationHolder(long periodDurationUs, Representation representation, - boolean enableEventMessageTrack, boolean enableCea608Track) { + /* package */ RepresentationHolder(long periodDurationUs, int trackType, + Representation representation, boolean enableEventMessageTrack, boolean enableCea608Track) { this.periodDurationUs = periodDurationUs; this.representation = representation; String containerMimeType = representation.format.containerMimeType; @@ -415,7 +415,7 @@ public class DefaultDashChunkSource implements DashChunkSource { } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. - extractorWrapper = new ChunkExtractorWrapper(extractor, representation.format); + extractorWrapper = new ChunkExtractorWrapper(extractor, trackType, representation.format); } segmentIndex = representation.getIndex(); } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index f2e4c57298..1069527989 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -102,7 +102,7 @@ public class DefaultSsChunkSource implements SsChunkSource { FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, null, track); - extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format); + extractorWrappers[i] = new ChunkExtractorWrapper(extractor, streamElement.type, format); } } From b0df6dce9823e11c45c881abf31ac8cb9a9ba92f Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 15:38:04 -0700 Subject: [PATCH 0032/1327] Fix moe config ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166914821 --- .../ProgressiveDownloadActionTest.java | 123 ------ .../dash/offline/DashDownloadTestData.java | 102 +++++ .../dash/offline/DashDownloaderTest.java | 406 ++++++++++++++++++ .../source/dash/offline/DashDownloader.java | 173 ++++++++ .../source/hls/offline/HlsDownloadAction.java | 83 ---- .../offline/SsDownloadAction.java | 86 ---- 6 files changed, 681 insertions(+), 292 deletions(-) delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java create mode 100644 library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java create mode 100644 library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java delete mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java delete mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java deleted file mode 100644 index ec45ea01c7..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java +++ /dev/null @@ -1,123 +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.offline; - -import android.test.InstrumentationTestCase; -import com.google.android.exoplayer2.testutil.TestUtil; -import com.google.android.exoplayer2.upstream.DummyDataSource; -import com.google.android.exoplayer2.upstream.cache.Cache; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import org.mockito.Mockito; - -/** - * Unit tests for {@link ProgressiveDownloadAction}. - */ -public class ProgressiveDownloadActionTest extends InstrumentationTestCase { - - public void testDownloadActionIsNotRemoveAction() throws Exception { - ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); - assertFalse(action.isRemoveAction()); - } - - public void testRemoveActionIsRemoveAction() throws Exception { - ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true); - assertTrue(action2.isRemoveAction()); - } - - public void testCreateDownloader() throws Exception { - TestUtil.setUpMockito(this); - ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); - DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper( - Mockito.mock(Cache.class), DummyDataSource.FACTORY); - assertNotNull(action.createDownloader(constructorHelper)); - } - - public void testSameUriCacheKeyDifferentAction_IsSameMedia() throws Exception { - ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true); - ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, false); - assertTrue(action1.isSameMedia(action2)); - } - - public void testNullCacheKeyDifferentUriAction_IsNotSameMedia() throws Exception { - ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri2", null, true); - ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, false); - assertFalse(action3.isSameMedia(action4)); - } - - public void testSameCacheKeyDifferentUriAction_IsSameMedia() throws Exception { - ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri2", "key", true); - ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", "key", false); - assertTrue(action5.isSameMedia(action6)); - } - - public void testSameUriDifferentCacheKeyAction_IsNotSameMedia() throws Exception { - ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true); - ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", false); - assertFalse(action7.isSameMedia(action8)); - } - - public void testEquals() throws Exception { - ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true); - assertTrue(action1.equals(action1)); - - ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true); - ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri", null, true); - assertTrue(action2.equals(action3)); - - ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, true); - ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri", null, false); - assertFalse(action4.equals(action5)); - - ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", null, true); - ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true); - assertFalse(action6.equals(action7)); - - ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", true); - ProgressiveDownloadAction action9 = new ProgressiveDownloadAction("uri", "key", true); - assertFalse(action8.equals(action9)); - - ProgressiveDownloadAction action10 = new ProgressiveDownloadAction("uri", null, true); - ProgressiveDownloadAction action11 = new ProgressiveDownloadAction("uri2", null, true); - assertFalse(action10.equals(action11)); - } - - public void testSerializerGetType() throws Exception { - ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); - assertNotNull(action.getType()); - } - - public void testSerializerWriteRead() throws Exception { - doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri1", null, false)); - doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri2", "key", true)); - } - - private void doTestSerializationRoundTrip(ProgressiveDownloadAction action1) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - DataOutputStream output = new DataOutputStream(out); - action1.writeToStream(output); - - ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); - DataInputStream input = new DataInputStream(in); - DownloadAction action2 = ProgressiveDownloadAction.DESERIALIZER.readFromStream(input); - - assertEquals(action1, action2); - } - -} diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java new file mode 100644 index 0000000000..220adfb3c5 --- /dev/null +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java @@ -0,0 +1,102 @@ +/* + * 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.dash.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.util.ClosedSource; + +/** + * Data for DASH downloading tests. + */ +@ClosedSource(reason = "Not ready yet") +/* package */ interface DashDownloadTestData { + + Uri TEST_MPD_URI = Uri.parse("test.mpd"); + + byte[] TEST_MPD = + ("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + // Bounded range data + + " \n" + // Unbounded range data + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + // This segment list has a 1 second offset to make sure the progressive download order + + " \n" + + " \n" + + " \n" // 1s offset + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "").getBytes(); + + byte[] TEST_MPD_NO_INDEX = + ("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "").getBytes(); +} diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java new file mode 100644 index 0000000000..f5e00d2cec --- /dev/null +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java @@ -0,0 +1,406 @@ +/* + * 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.dash.offline; + +import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD; +import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD_NO_INDEX; +import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD_URI; +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty; +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertDataCached; + +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.DownloadException; +import com.google.android.exoplayer2.offline.Downloader.ProgressListener; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import com.google.android.exoplayer2.testutil.FakeDataSet; +import com.google.android.exoplayer2.testutil.FakeDataSource; +import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import com.google.android.exoplayer2.util.Util; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import org.mockito.InOrder; +import org.mockito.Mockito; + +/** + * Unit tests for {@link DashDownloader}. + */ +public class DashDownloaderTest extends InstrumentationTestCase { + + private SimpleCache cache; + private File tempFolder; + + @Override + public void setUp() throws Exception { + super.setUp(); + TestUtil.setUpMockito(this); + 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 testGetManifest() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + DashManifest manifest = dashDownloader.getManifest(); + + assertNotNull(manifest); + assertCachedData(cache, fakeDataSet); + } + + public void testDownloadManifestFailure() throws Exception { + byte[] testMpdFirstPart = Arrays.copyOf(TEST_MPD, 10); + byte[] testMpdSecondPart = Arrays.copyOfRange(TEST_MPD, 10, TEST_MPD.length); + FakeDataSet fakeDataSet = new FakeDataSet() + .newData(TEST_MPD_URI) + .appendReadData(testMpdFirstPart) + .appendReadError(new IOException()) + .appendReadData(testMpdSecondPart) + .endData(); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + // fails on the first try + try { + dashDownloader.getManifest(); + fail(); + } catch (IOException e) { + // ignore + } + assertDataCached(cache, TEST_MPD_URI, testMpdFirstPart); + + // on the second try it downloads the rest of the data + DashManifest manifest = dashDownloader.getManifest(); + + assertNotNull(manifest); + assertCachedData(cache, fakeDataSet); + } + + public void testDownloadRepresentation() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); + dashDownloader.download(null); + + assertCachedData(cache, fakeDataSet); + } + + public void testDownloadRepresentationInSmallParts() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .newData("audio_segment_1") + .appendReadData(TestUtil.buildTestData(10)) + .appendReadData(TestUtil.buildTestData(10)) + .appendReadData(TestUtil.buildTestData(10)) + .endData() + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); + dashDownloader.download(null); + + assertCachedData(cache, fakeDataSet); + } + + public void testDownloadRepresentations() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + dashDownloader.selectRepresentations( + new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)}); + dashDownloader.download(null); + + assertCachedData(cache, fakeDataSet); + } + + public void testDownloadAllRepresentations() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3) + .setRandomData("period_2_segment_1", 1) + .setRandomData("period_2_segment_2", 2) + .setRandomData("period_2_segment_3", 3); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + // dashDownloader.selectRepresentations() isn't called + dashDownloader.download(null); + assertCachedData(cache, fakeDataSet); + dashDownloader.remove(); + + // select something random + dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); + // clear selection + dashDownloader.selectRepresentations(null); + dashDownloader.download(null); + assertCachedData(cache, fakeDataSet); + dashDownloader.remove(); + + dashDownloader.selectRepresentations(new RepresentationKey[0]); + dashDownloader.download(null); + assertCachedData(cache, fakeDataSet); + dashDownloader.remove(); + } + + public void testProgressiveDownload() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3); + FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet); + Factory factory = Mockito.mock(Factory.class); + Mockito.when(factory.createDataSource()).thenReturn(fakeDataSource); + DashDownloader dashDownloader = new DashDownloader(TEST_MPD_URI, + new DownloaderConstructorHelper(cache, factory)); + + dashDownloader.selectRepresentations( + new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)}); + dashDownloader.download(null); + + DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs(); + assertEquals(8, openedDataSpecs.length); + assertEquals(TEST_MPD_URI, openedDataSpecs[0].uri); + assertEquals("audio_init_data", openedDataSpecs[1].uri.getPath()); + assertEquals("audio_segment_1", openedDataSpecs[2].uri.getPath()); + assertEquals("text_segment_1", openedDataSpecs[3].uri.getPath()); + assertEquals("audio_segment_2", openedDataSpecs[4].uri.getPath()); + assertEquals("text_segment_2", openedDataSpecs[5].uri.getPath()); + assertEquals("audio_segment_3", openedDataSpecs[6].uri.getPath()); + assertEquals("text_segment_3", openedDataSpecs[7].uri.getPath()); + } + + public void testProgressiveDownloadSeparatePeriods() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("period_2_segment_1", 1) + .setRandomData("period_2_segment_2", 2) + .setRandomData("period_2_segment_3", 3); + FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet); + Factory factory = Mockito.mock(Factory.class); + Mockito.when(factory.createDataSource()).thenReturn(fakeDataSource); + DashDownloader dashDownloader = new DashDownloader(TEST_MPD_URI, + new DownloaderConstructorHelper(cache, factory)); + + dashDownloader.selectRepresentations( + new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(1, 0, 0)}); + dashDownloader.download(null); + + DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs(); + assertEquals(8, openedDataSpecs.length); + assertEquals(TEST_MPD_URI, openedDataSpecs[0].uri); + assertEquals("audio_init_data", openedDataSpecs[1].uri.getPath()); + assertEquals("audio_segment_1", openedDataSpecs[2].uri.getPath()); + assertEquals("audio_segment_2", openedDataSpecs[3].uri.getPath()); + assertEquals("audio_segment_3", openedDataSpecs[4].uri.getPath()); + assertEquals("period_2_segment_1", openedDataSpecs[5].uri.getPath()); + assertEquals("period_2_segment_2", openedDataSpecs[6].uri.getPath()); + assertEquals("period_2_segment_3", openedDataSpecs[7].uri.getPath()); + } + + public void testDownloadRepresentationFailure() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .newData("audio_segment_2") + .appendReadData(TestUtil.buildTestData(2)) + .appendReadError(new IOException()) + .appendReadData(TestUtil.buildTestData(3)) + .endData() + .setRandomData("audio_segment_3", 6); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); + // downloadRepresentations fails on the first try + try { + dashDownloader.download(null); + fail(); + } catch (IOException e) { + // ignore + } + dashDownloader.download(null); + + assertCachedData(cache, fakeDataSet); + } + + public void testCounters() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .newData("audio_segment_2") + .appendReadData(TestUtil.buildTestData(2)) + .appendReadError(new IOException()) + .appendReadData(TestUtil.buildTestData(3)) + .endData() + .setRandomData("audio_segment_3", 6); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + assertCounters(dashDownloader, C.LENGTH_UNSET, C.LENGTH_UNSET, C.LENGTH_UNSET); + + dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); + dashDownloader.init(); + assertCounters(dashDownloader, C.LENGTH_UNSET, C.LENGTH_UNSET, C.LENGTH_UNSET); + + // downloadRepresentations fails after downloading init data, segment 1 and 2 bytes in segment 2 + try { + dashDownloader.download(null); + fail(); + } catch (IOException e) { + // ignore + } + dashDownloader.init(); + assertCounters(dashDownloader, 4, 2, 10 + 4 + 2); + + dashDownloader.download(null); + + assertCounters(dashDownloader, 4, 4, 10 + 4 + 5 + 6); + } + + public void testListener() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); + ProgressListener mockListener = Mockito.mock(ProgressListener.class); + dashDownloader.download(mockListener); + InOrder inOrder = Mockito.inOrder(mockListener); + inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 0.0f, 0); + inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 25.0f, 10); + inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 50.0f, 14); + inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 75.0f, 19); + inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 100.0f, 25); + inOrder.verifyNoMoreInteractions(); + } + + public void testRemoveAll() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + dashDownloader.selectRepresentations( + new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)}); + dashDownloader.download(null); + + dashDownloader.remove(); + + assertCacheEmpty(cache); + } + + public void testRepresentationWithoutIndex() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD_NO_INDEX) + .setRandomData("test_segment_1", 4); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); + dashDownloader.init(); + try { + dashDownloader.download(null); + fail(); + } catch (DownloadException e) { + // expected exception. + } + dashDownloader.remove(); + + assertCacheEmpty(cache); + } + + public void testSelectRepresentationsClearsPreviousSelection() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + dashDownloader.selectRepresentations( + new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)}); + dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); + dashDownloader.download(null); + + assertCachedData(cache, fakeDataSet); + } + + private DashDownloader getDashDownloader(FakeDataSet fakeDataSet) { + Factory factory = new Factory(null).setFakeDataSet(fakeDataSet); + return new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory)); + } + + private static void assertCounters(DashDownloader dashDownloader, int totalSegments, + int downloadedSegments, int downloadedBytes) { + assertEquals(totalSegments, dashDownloader.getTotalSegments()); + assertEquals(downloadedSegments, dashDownloader.getDownloadedSegments()); + assertEquals(downloadedBytes, dashDownloader.getDownloadedBytes()); + } + +} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java new file mode 100644 index 0000000000..558adca7bd --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java @@ -0,0 +1,173 @@ +/* + * 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.dash.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.ChunkIndex; +import com.google.android.exoplayer2.offline.DownloadException; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.SegmentDownloader; +import com.google.android.exoplayer2.source.dash.DashSegmentIndex; +import com.google.android.exoplayer2.source.dash.DashUtil; +import com.google.android.exoplayer2.source.dash.DashWrappingSegmentIndex; +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.Period; +import com.google.android.exoplayer2.source.dash.manifest.RangedUri; +import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class to download DASH streams. + * + *

    Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link + * #getDownloadedBytes()}, this class isn't thread safe. + * + *

    Example usage: + * + *

    + * {@code
    + * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor());
    + * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
    + * DownloaderConstructorHelper constructorHelper =
    + *     new DownloaderConstructorHelper(cache, factory);
    + * DashDownloader dashDownloader = new DashDownloader(manifestUrl, constructorHelper);
    + * // Select the first representation of the first adaptation set of the first period
    + * dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
    + * dashDownloader.download(new ProgressListener() {
    + *   @Override
    + *   public void onDownloadProgress(Downloader downloader, float downloadPercentage,
    + *       long downloadedBytes) {
    + *     // Invoked periodically during the download.
    + *   }
    + * });
    + * // Access downloaded data using CacheDataSource
    + * CacheDataSource cacheDataSource =
    + *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);}
    + * 
    + */ +public final class DashDownloader extends SegmentDownloader { + + /** + * @see SegmentDownloader#SegmentDownloader(Uri, DownloaderConstructorHelper) + */ + public DashDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) { + super(manifestUri, constructorHelper); + } + + @Override + public DashManifest getManifest(DataSource dataSource, Uri uri) throws IOException { + return DashUtil.loadManifest(dataSource, uri); + } + + @Override + protected List getAllSegments(DataSource dataSource, DashManifest manifest, + boolean allowIndexLoadErrors) throws InterruptedException, IOException { + ArrayList segments = new ArrayList<>(); + for (int periodIndex = 0; periodIndex < manifest.getPeriodCount(); periodIndex++) { + List adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; + for (int adaptationIndex = 0; adaptationIndex < adaptationSets.size(); adaptationIndex++) { + AdaptationSet adaptationSet = adaptationSets.get(adaptationIndex); + RepresentationKey[] keys = new RepresentationKey[adaptationSet.representations.size()]; + for (int i = 0; i < keys.length; i++) { + keys[i] = new RepresentationKey(periodIndex, adaptationIndex, i); + } + segments.addAll(getSegments(dataSource, manifest, keys, allowIndexLoadErrors)); + } + } + return segments; + } + + @Override + protected List getSegments(DataSource dataSource, DashManifest manifest, + RepresentationKey[] keys, boolean allowIndexLoadErrors) + throws InterruptedException, IOException { + ArrayList segments = new ArrayList<>(); + for (RepresentationKey key : keys) { + DashSegmentIndex index; + try { + index = getSegmentIndex(dataSource, manifest, key); + if (index == null) { + // Loading succeeded but there was no index. This is always a failure. + throw new DownloadException("No index for representation: " + key); + } + } catch (IOException e) { + if (allowIndexLoadErrors) { + // Loading failed, but load errors are allowed. Advance to the next key. + continue; + } else { + throw e; + } + } + + int segmentCount = index.getSegmentCount(C.TIME_UNSET); + if (segmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { + throw new DownloadException("Unbounded index for representation: " + key); + } + + Period period = manifest.getPeriod(key.periodIndex); + Representation representation = period.adaptationSets.get(key.adaptationSetIndex) + .representations.get(key.representationIndex); + long startUs = C.msToUs(period.startMs); + String baseUrl = representation.baseUrl; + RangedUri initializationUri = representation.getInitializationUri(); + if (initializationUri != null) { + addSegment(segments, startUs, baseUrl, initializationUri); + } + RangedUri indexUri = representation.getIndexUri(); + if (indexUri != null) { + addSegment(segments, startUs, baseUrl, indexUri); + } + + int firstSegmentNum = index.getFirstSegmentNum(); + int lastSegmentNum = firstSegmentNum + segmentCount - 1; + for (int j = firstSegmentNum; j <= lastSegmentNum; j++) { + addSegment(segments, startUs + index.getTimeUs(j), baseUrl, index.getSegmentUrl(j)); + } + } + return segments; + } + + /** + * Returns DashSegmentIndex for given representation. + */ + private DashSegmentIndex getSegmentIndex(DataSource dataSource, DashManifest manifest, + RepresentationKey key) throws IOException, InterruptedException { + AdaptationSet adaptationSet = manifest.getPeriod(key.periodIndex).adaptationSets.get( + key.adaptationSetIndex); + Representation representation = adaptationSet.representations.get(key.representationIndex); + DashSegmentIndex index = representation.getIndex(); + if (index != null) { + return index; + } + ChunkIndex seekMap = DashUtil.loadChunkIndex(dataSource, adaptationSet.type, representation); + return seekMap == null ? null : new DashWrappingSegmentIndex(seekMap); + } + + private static void addSegment(ArrayList segments, long startTimeUs, String baseUrl, + RangedUri rangedUri) { + DataSpec dataSpec = new DataSpec(rangedUri.resolveUri(baseUrl), rangedUri.start, + rangedUri.length, null); + segments.add(new Segment(startTimeUs, dataSpec)); + } + +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java deleted file mode 100644 index 3c23e25796..0000000000 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java +++ /dev/null @@ -1,83 +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.hls.offline; - -import android.net.Uri; -import com.google.android.exoplayer2.offline.DownloadAction; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.offline.SegmentDownloadAction; -import com.google.android.exoplayer2.util.ClosedSource; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -/** An action to download or remove downloaded HLS streams. */ -@ClosedSource(reason = "Not ready yet") -public final class HlsDownloadAction extends SegmentDownloadAction { - - public static final Deserializer DESERIALIZER = new SegmentDownloadActionDeserializer() { - - @Override - public String getType() { - return TYPE; - } - - @Override - protected String readKey(DataInputStream input) throws IOException { - return input.readUTF(); - } - - @Override - protected String[] createKeyArray(int keyCount) { - return new String[0]; - } - - @Override - protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, - String[] keys) { - return new HlsDownloadAction(manifestUri, removeAction, keys); - } - - }; - - private static final String TYPE = "HlsDownloadAction"; - - /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, Object[]) */ - public HlsDownloadAction(Uri manifestUri, boolean removeAction, String... keys) { - super(manifestUri, removeAction, keys); - } - - @Override - public String getType() { - return TYPE; - } - - @Override - public HlsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) - throws IOException { - HlsDownloader downloader = new HlsDownloader(manifestUri, constructorHelper); - if (!isRemoveAction()) { - downloader.selectRepresentations(keys); - } - return downloader; - } - - @Override - protected void writeKey(DataOutputStream output, String key) throws IOException { - output.writeUTF(key); - } - -} diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java deleted file mode 100644 index 7478062ef8..0000000000 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java +++ /dev/null @@ -1,86 +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.smoothstreaming.offline; - -import android.net.Uri; -import com.google.android.exoplayer2.offline.DownloadAction; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.offline.SegmentDownloadAction; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; -import com.google.android.exoplayer2.util.ClosedSource; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -/** An action to download or remove downloaded SmoothStreaming streams. */ -@ClosedSource(reason = "Not ready yet") -public final class SsDownloadAction extends SegmentDownloadAction { - - public static final Deserializer DESERIALIZER = - new SegmentDownloadActionDeserializer() { - - @Override - public String getType() { - return TYPE; - } - - @Override - protected TrackKey readKey(DataInputStream input) throws IOException { - return new TrackKey(input.readInt(), input.readInt()); - } - - @Override - protected TrackKey[] createKeyArray(int keyCount) { - return new TrackKey[keyCount]; - } - - @Override - protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, - TrackKey[] keys) { - return new SsDownloadAction(manifestUri, removeAction, keys); - } - - }; - - private static final String TYPE = "SsDownloadAction"; - - /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, Object[]) */ - public SsDownloadAction(Uri manifestUri, boolean removeAction, TrackKey... keys) { - super(manifestUri, removeAction, keys); - } - - @Override - public String getType() { - return TYPE; - } - - @Override - public SsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) - throws IOException { - SsDownloader downloader = new SsDownloader(manifestUri, constructorHelper); - if (!isRemoveAction()) { - downloader.selectRepresentations(keys); - } - return downloader; - } - - @Override - protected void writeKey(DataOutputStream output, TrackKey key) throws IOException { - output.writeInt(key.streamElementIndex); - output.writeInt(key.trackIndex); - } - -} From 6bf0b7f3de32b34cc58786474a302198cc18e421 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 15:51:36 -0700 Subject: [PATCH 0033/1327] Fix moe config II ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166916769 --- .../exoplayer2/source/dash/offline/DashDownloadTestData.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java index 220adfb3c5..50752c8a72 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java @@ -16,12 +16,10 @@ package com.google.android.exoplayer2.source.dash.offline; import android.net.Uri; -import com.google.android.exoplayer2.util.ClosedSource; /** * Data for DASH downloading tests. */ -@ClosedSource(reason = "Not ready yet") /* package */ interface DashDownloadTestData { Uri TEST_MPD_URI = Uri.parse("test.mpd"); From 49c2926e45b2d9ee4fc41dd834074c61c80e2c76 Mon Sep 17 00:00:00 2001 From: Shyri Villar Date: Wed, 30 Aug 2017 16:25:50 +0200 Subject: [PATCH 0034/1327] Add support for new codecs parameter string --- .../java/com/google/android/exoplayer2/util/MimeTypes.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 2d4a1ec96f..1c8bb62a75 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 @@ -184,9 +184,9 @@ public final class MimeTypes { return MimeTypes.VIDEO_H264; } else if (codec.startsWith("hev1") || codec.startsWith("hvc1")) { return MimeTypes.VIDEO_H265; - } else if (codec.startsWith("vp9")) { + } else if (codec.startsWith("vp9") || codec.startsWith("vp09")) { return MimeTypes.VIDEO_VP9; - } else if (codec.startsWith("vp8")) { + } else if (codec.startsWith("vp8") || codec.startsWith("vp08")) { return MimeTypes.VIDEO_VP8; } else if (codec.startsWith("mp4a")) { return MimeTypes.AUDIO_AAC; From 0c7f11606f0eaf3660511d4a478fc80de4c2e63d Mon Sep 17 00:00:00 2001 From: Danny Brain Date: Thu, 31 Aug 2017 14:31:18 +1000 Subject: [PATCH 0035/1327] #3215 Additional secure DummySurface device exclusions --- .../android/exoplayer2/video/DummySurface.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 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 a1820ed7a1..8551f2541d 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 @@ -43,6 +43,7 @@ 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; @@ -152,9 +153,16 @@ public final class DummySurface extends Surface { * * @param context Any {@link Context}. */ - @SuppressWarnings("unused") // Context may be needed in the future for better targeting. + @SuppressWarnings("unused") private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { - return Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER); + return (Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER)) + || (Util.SDK_INT >= 24 && Util.SDK_INT < 26 + && !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, From 049d41db2a4a697e23a39271a9a2758c8bd90fbd Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 30 Aug 2017 07:03:41 -0700 Subject: [PATCH 0036/1327] Add license notes for extensions with non-Google dependencies Issue: #3197 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166988657 --- extensions/ffmpeg/README.md | 8 ++++++++ extensions/flac/README.md | 8 ++++++++ extensions/okhttp/README.md | 8 ++++++++ extensions/opus/README.md | 8 ++++++++ extensions/rtmp/README.md | 8 ++++++++ extensions/vp9/README.md | 8 ++++++++ 6 files changed, 48 insertions(+) diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index 57b637d1e2..b29c836887 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -3,6 +3,14 @@ The FFmpeg extension provides `FfmpegAudioRenderer`, which uses FFmpeg for decoding and can render audio encoded in a variety of formats. +## License note ## + +Please note that whilst the code in this repository is licensed under +[Apache 2.0][], using this extension also requires building and including one or +more external libraries as described below. These are licensed separately. + +[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE + ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on diff --git a/extensions/flac/README.md b/extensions/flac/README.md index 113b41a93d..cd0f2efe47 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -3,6 +3,14 @@ The Flac extension provides `FlacExtractor` and `LibflacAudioRenderer`, which use libFLAC (the Flac decoding library) to extract and decode FLAC audio. +## License note ## + +Please note that whilst the code in this repository is licensed under +[Apache 2.0][], using this extension also requires building and including one or +more external libraries as described below. These are licensed separately. + +[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE + ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on diff --git a/extensions/okhttp/README.md b/extensions/okhttp/README.md index f84d0c35f2..e40535d4e8 100644 --- a/extensions/okhttp/README.md +++ b/extensions/okhttp/README.md @@ -6,6 +6,14 @@ The OkHttp extension is an [HttpDataSource][] implementation using Square's [HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html [OkHttp]: https://square.github.io/okhttp/ +## License note ## + +Please note that whilst the code in this repository is licensed under +[Apache 2.0][], using this extension requires depending on OkHttp, which is +licensed separately. + +[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE + ## Getting the extension ## The easiest way to use the extension is to add it as a gradle dependency: diff --git a/extensions/opus/README.md b/extensions/opus/README.md index d766e8c9c4..15c3e5413d 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -3,6 +3,14 @@ The Opus extension provides `LibopusAudioRenderer`, which uses libopus (the Opus decoding library) to decode Opus audio. +## License note ## + +Please note that whilst the code in this repository is licensed under +[Apache 2.0][], using this extension also requires building and including one or +more external libraries as described below. These are licensed separately. + +[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE + ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on diff --git a/extensions/rtmp/README.md b/extensions/rtmp/README.md index 7e6bc0d641..fb822b8326 100644 --- a/extensions/rtmp/README.md +++ b/extensions/rtmp/README.md @@ -7,6 +7,14 @@ streams using [LibRtmp Client for Android][]. [RTMP]: https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol [LibRtmp Client for Android]: https://github.com/ant-media/LibRtmp-Client-for-Android +## License note ## + +Please note that whilst the code in this repository is licensed under +[Apache 2.0][], using this extension requires depending on LibRtmp Client for +Android, which is licensed separately. + +[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE + ## Getting the extension ## The easiest way to use the extension is to add it as a gradle dependency: diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 7bce4a2a25..941b413c09 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -3,6 +3,14 @@ The VP9 extension provides `LibvpxVideoRenderer`, which uses libvpx (the VPx decoding library) to decode VP9 video. +## License note ## + +Please note that whilst the code in this repository is licensed under +[Apache 2.0][], using this extension also requires building and including one or +more external libraries as described below. These are licensed separately. + +[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE + ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on From e80a93d7990db645707f579df58cb380abb1edd4 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 30 Aug 2017 10:20:37 -0700 Subject: [PATCH 0037/1327] Use UTF-8 everywhere UTF-8 is the default charset on Android so this should be a no-op change, but makes the code portable (in case it runs on another platform). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167011583 --- .../com/google/android/exoplayer2/util/ParsableByteArray.java | 3 ++- .../src/main/java/com/google/android/exoplayer2/util/Util.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index 2a907e5955..70cb584085 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.util; +import com.google.android.exoplayer2.C; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -428,7 +429,7 @@ public final class ParsableByteArray { * @return The string encoded by the bytes. */ public String readString(int length) { - return readString(length, Charset.defaultCharset()); + return readString(length, Charset.forName(C.UTF8_NAME)); } /** 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 b958a54244..519919f129 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 @@ -253,7 +253,7 @@ public final class Util { * @return The code points encoding using UTF-8. */ public static byte[] getUtf8Bytes(String value) { - return value.getBytes(Charset.defaultCharset()); // UTF-8 is the default on Android. + return value.getBytes(Charset.forName(C.UTF8_NAME)); } /** From 84c13ccbf3daf7814099137f2cfa27957893c664 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 30 Aug 2017 10:32:47 -0700 Subject: [PATCH 0038/1327] Support setRepeatMode (and move shuffle action to PlaybackController) ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167013507 --- .../DefaultPlaybackController.java | 53 ++++++++++--- .../mediasession/MediaSessionConnector.java | 79 +++++++++++-------- .../RepeatModeActionProvider.java | 10 +-- .../mediasession/TimelineQueueNavigator.java | 5 -- 4 files changed, 93 insertions(+), 54 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java index e01d6a48db..95ebcde095 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java @@ -21,6 +21,7 @@ import android.support.v4.media.session.PlaybackStateCompat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.util.RepeatModeUtil; /** * A default implementation of {@link MediaSessionConnector.PlaybackController}. @@ -40,33 +41,37 @@ public class DefaultPlaybackController implements MediaSessionConnector.Playback private static final long BASE_ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE - | PlaybackStateCompat.ACTION_STOP; + | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED + | PlaybackStateCompat.ACTION_SET_REPEAT_MODE; protected final long rewindIncrementMs; protected final long fastForwardIncrementMs; + protected final int repeatToggleModes; /** * Creates a new instance. *

    - * Equivalent to {@code DefaultPlaybackController( - * DefaultPlaybackController.DEFAULT_REWIND_MS, - * DefaultPlaybackController.DEFAULT_FAST_FORWARD_MS)}. + * Equivalent to {@code DefaultPlaybackController(DefaultPlaybackController.DEFAULT_REWIND_MS, + * DefaultPlaybackController.DEFAULT_FAST_FORWARD_MS, + * MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES)}. */ public DefaultPlaybackController() { - this(DEFAULT_REWIND_MS, DEFAULT_FAST_FORWARD_MS); + this(DEFAULT_REWIND_MS, DEFAULT_FAST_FORWARD_MS, + MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES); } /** * Creates a new instance with the given fast forward and rewind increments. - * - * @param rewindIncrementMs The rewind increment in milliseconds. A zero or negative value will + * @param rewindIncrementMs The rewind increment in milliseconds. A zero or negative value will * cause the rewind action to be disabled. * @param fastForwardIncrementMs The fast forward increment in milliseconds. A zero or negative - * value will cause the fast forward action to be removed. + * @param repeatToggleModes The available repeatToggleModes. */ - public DefaultPlaybackController(long rewindIncrementMs, long fastForwardIncrementMs) { + public DefaultPlaybackController(long rewindIncrementMs, long fastForwardIncrementMs, + @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { this.rewindIncrementMs = rewindIncrementMs; this.fastForwardIncrementMs = fastForwardIncrementMs; + this.repeatToggleModes = repeatToggleModes; } @Override @@ -127,6 +132,36 @@ public class DefaultPlaybackController implements MediaSessionConnector.Playback player.stop(); } + @Override + public void onSetShuffleMode(Player player, int shuffleMode) { + player.setShuffleModeEnabled(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL + || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP); + } + + @Override + public void onSetRepeatMode(Player player, int repeatMode) { + int selectedExoPlayerRepeatMode = player.getRepeatMode(); + switch (repeatMode) { + case PlaybackStateCompat.REPEAT_MODE_ALL: + case PlaybackStateCompat.REPEAT_MODE_GROUP: + if ((repeatToggleModes & RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL) != 0) { + selectedExoPlayerRepeatMode = Player.REPEAT_MODE_ALL; + } + break; + case PlaybackStateCompat.REPEAT_MODE_ONE: + if ((repeatToggleModes & RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE) != 0) { + selectedExoPlayerRepeatMode = Player.REPEAT_MODE_ONE; + } + break; + default: + selectedExoPlayerRepeatMode = Player.REPEAT_MODE_OFF; + break; + } + player.setRepeatMode(selectedExoPlayerRepeatMode); + } + + // CommandReceiver implementation. + @Override public String[] getCommands() { return null; diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index a64f163733..4c7ad123f3 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -39,6 +39,8 @@ 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.ErrorMessageProvider; +import com.google.android.exoplayer2.util.RepeatModeUtil; + import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -73,6 +75,13 @@ public final class MediaSessionConnector { ExoPlayerLibraryInfo.registerModule("goog.exo.mediasession"); } + /** + * The default repeat toggle modes which is the bitmask of + * {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ONE} and + * {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ALL}. + */ + public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = + RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL; public static final String EXTRAS_PITCH = "EXO_PITCH"; private static final int BASE_MEDIA_SESSION_FLAGS = MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS; @@ -145,14 +154,17 @@ public final class MediaSessionConnector { long ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SEEK_TO | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND - | PlaybackStateCompat.ACTION_STOP; + | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SET_REPEAT_MODE + | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED; /** * Returns the actions which are supported by the controller. The supported actions must be a * bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE}, * {@link PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE}, * {@link PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD}, - * {@link PlaybackStateCompat#ACTION_REWIND} and {@link PlaybackStateCompat#ACTION_STOP}. + * {@link PlaybackStateCompat#ACTION_REWIND}, {@link PlaybackStateCompat#ACTION_STOP}, + * {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE} and + * {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE_ENABLED}. * * @param player The player. * @return The bitmask of the supported media actions. @@ -182,6 +194,14 @@ public final class MediaSessionConnector { * See {@link MediaSessionCompat.Callback#onStop()}. */ void onStop(Player player); + /** + * See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}. + */ + void onSetShuffleMode(Player player, int shuffleMode); + /** + * See {@link MediaSessionCompat.Callback#onSetRepeatMode(int)}. + */ + void onSetRepeatMode(Player player, int repeatMode); } /** @@ -191,15 +211,13 @@ public final class MediaSessionConnector { public interface QueueNavigator extends CommandReceiver { long ACTIONS = PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM - | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS - | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED; + | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; /** * Returns the actions which are supported by the navigator. The supported actions must be a * bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM}, * {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}, - * {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}, - * {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE_ENABLED}. + * {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}. * * @param player The {@link Player}. * @return The bitmask of the supported media actions. @@ -241,10 +259,6 @@ public final class MediaSessionConnector { * See {@link MediaSessionCompat.Callback#onSkipToNext()}. */ void onSkipToNext(Player player); - /** - * See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}. - */ - void onSetShuffleMode(Player player, int shuffleMode); } /** @@ -429,8 +443,7 @@ public final class MediaSessionConnector { /** * Sets the {@link QueueNavigator} to handle queue navigation actions {@code ACTION_SKIP_TO_NEXT}, - * {@code ACTION_SKIP_TO_PREVIOUS}, {@code ACTION_SKIP_TO_QUEUE_ITEM} and - * {@code ACTION_SET_SHUFFLE_MODE_ENABLED}. + * {@code ACTION_SKIP_TO_PREVIOUS} and {@code ACTION_SKIP_TO_QUEUE_ITEM}. * * @param queueNavigator The queue navigator. */ @@ -736,6 +749,28 @@ public final class MediaSessionConnector { } } + @Override + public void onSetShuffleModeEnabled(boolean enabled) { + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) { + playbackController.onSetShuffleMode(player, enabled + ? PlaybackStateCompat.SHUFFLE_MODE_ALL : PlaybackStateCompat.SHUFFLE_MODE_NONE); + } + } + + @Override + public void onSetShuffleMode(int shuffleMode) { + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) { + playbackController.onSetShuffleMode(player, shuffleMode); + } + } + + @Override + public void onSetRepeatMode(int repeatMode) { + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SET_REPEAT_MODE)) { + playbackController.onSetRepeatMode(player, repeatMode); + } + } + @Override public void onSkipToNext() { if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) { @@ -757,11 +792,6 @@ public final class MediaSessionConnector { } } - @Override - public void onSetRepeatMode(int repeatMode) { - // implemented as custom action - } - @Override public void onCustomAction(@NonNull String action, @Nullable Bundle extras) { Map actionMap = customActionMap; @@ -842,21 +872,6 @@ public final class MediaSessionConnector { } } - @Override - public void onSetShuffleModeEnabled(boolean enabled) { - if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) { - queueNavigator.onSetShuffleMode(player, enabled - ? PlaybackStateCompat.SHUFFLE_MODE_ALL : PlaybackStateCompat.SHUFFLE_MODE_NONE); - } - } - - @Override - public void onSetShuffleMode(int shuffleMode) { - if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) { - queueNavigator.onSetShuffleMode(player, shuffleMode); - } - } - @Override public void onAddQueueItem(MediaDescriptionCompat description) { if (queueEditor != null) { diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java index db0190de0f..b4cb3c73d0 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java @@ -25,12 +25,6 @@ import com.google.android.exoplayer2.util.RepeatModeUtil; */ public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider { - /** - * The default repeat toggle modes. - */ - public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = - RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL; - private static final String ACTION_REPEAT_MODE = "ACTION_EXO_REPEAT_MODE"; private final Player player; @@ -44,13 +38,13 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus * Creates a new instance. *

    * Equivalent to {@code RepeatModeActionProvider(context, player, - * RepeatModeActionProvider.DEFAULT_REPEAT_TOGGLE_MODES)}. + * MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES)}. * * @param context The context. * @param player The player on which to toggle the repeat mode. */ public RepeatModeActionProvider(Context context, Player player) { - this(context, player, DEFAULT_REPEAT_TOGGLE_MODES); + this(context, player, MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES); } /** diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 777949863d..0484c0b641 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -161,11 +161,6 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu } } - @Override - public void onSetShuffleMode(Player player, int shuffleMode) { - player.setShuffleModeEnabled(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL); - } - // CommandReceiver implementation. @Override From 0b78837f35f54acc3ef41f5ceeb3b634df1cd142 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 30 Aug 2017 10:51:37 -0700 Subject: [PATCH 0039/1327] Fix ContentDataSource bytesRemaining calculation The bytesRemaining didn't always take into account any skipped bytes, which meant that reaching the end of the file was not correctly detected in read(). Issue: #3216 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167016672 --- .../upstream/ContentDataSourceTest.java | 27 +++++++++++++++++++ .../upstream/ContentDataSource.java | 10 ++++--- .../android/exoplayer2/testutil/TestUtil.java | 2 +- 3 files changed, 34 insertions(+), 5 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 834e7e1374..2b70c83ca5 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,9 +23,12 @@ 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.C; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.FileNotFoundException; import java.io.IOException; +import java.util.Arrays; /** * Unit tests for {@link ContentDataSource}. @@ -35,6 +38,9 @@ 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 int TEST_DATA_OFFSET = 1; + private static final int TEST_DATA_LENGTH = 1023; + public void testReadValidUri() throws Exception { ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); Uri contentUri = new Uri.Builder() @@ -64,6 +70,27 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { } } + public void testReadFromOffsetToEndOfInput() throws Exception { + ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); + Uri contentUri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(AUTHORITY) + .path(DATA_PATH).build(); + try { + DataSpec dataSpec = new DataSpec(contentUri, TEST_DATA_OFFSET, C.LENGTH_UNSET, null); + long length = dataSource.open(dataSpec); + assertEquals(TEST_DATA_LENGTH, length); + byte[] expectedData = Arrays.copyOfRange( + TestUtil.getByteArray(getInstrumentation(), DATA_PATH), TEST_DATA_OFFSET, + TEST_DATA_OFFSET + TEST_DATA_LENGTH); + byte[] readData = TestUtil.readToEnd(dataSource); + MoreAsserts.assertEquals(expectedData, readData); + assertEquals(C.RESULT_END_OF_INPUT, dataSource.read(new byte[1], 0, 1)); + } finally { + dataSource.close(); + } + } + /** * A {@link ContentProvider} for the test. */ 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 d118b91378..c37599eccc 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 @@ -76,8 +76,8 @@ public final class ContentDataSource implements DataSource { throw new FileNotFoundException("Could not open file descriptor for: " + uri); } inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); - long assertStartOffset = assetFileDescriptor.getStartOffset(); - long skipped = inputStream.skip(assertStartOffset + dataSpec.position) - assertStartOffset; + long assetStartOffset = assetFileDescriptor.getStartOffset(); + long skipped = inputStream.skip(assetStartOffset + dataSpec.position) - assetStartOffset; if (skipped != dataSpec.position) { // We expect the skip to be satisfied in full. If it isn't then we're probably trying to // skip beyond the end of the data. @@ -86,8 +86,8 @@ public final class ContentDataSource implements DataSource { if (dataSpec.length != C.LENGTH_UNSET) { bytesRemaining = dataSpec.length; } else { - bytesRemaining = assetFileDescriptor.getLength(); - if (bytesRemaining == AssetFileDescriptor.UNKNOWN_LENGTH) { + long assetFileDescriptorLength = assetFileDescriptor.getLength(); + if (assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH) { // The asset must extend to the end of the file. bytesRemaining = inputStream.available(); if (bytesRemaining == 0) { @@ -96,6 +96,8 @@ public final class ContentDataSource implements DataSource { // case, so treat as unbounded. bytesRemaining = C.LENGTH_UNSET; } + } else { + bytesRemaining = assetFileDescriptorLength - skipped; } } } catch (IOException e) { 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 5819a4b711..2e59b33c0b 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 @@ -181,7 +181,7 @@ public class TestUtil { byte[] expectedData) throws IOException { try { long length = dataSource.open(dataSpec); - Assert.assertEquals(length, expectedData.length); + Assert.assertEquals(expectedData.length, length); byte[] readData = TestUtil.readToEnd(dataSource); MoreAsserts.assertEquals(expectedData, readData); } finally { From 6bd0ba887c70e500724b4fc87d40db74f8d0c019 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 31 Aug 2017 04:13:13 -0700 Subject: [PATCH 0040/1327] Allow more aggressive switching for HLS with independent segments We currently switch without downloading overlapping segments, but we do not actually switch more aggressively. This change fixes this. Note there's an implicit assumption made that if one media playlist declares independent segments, the others will too. This is almost certainly true in practice, and if it's not the penalty isn't too bad (the player may try and switch to a higher quality variant one segment's worth of buffer too soon). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167120992 --- .../exoplayer2/source/hls/HlsChunkSource.java | 13 ++++++++----- 1 file changed, 8 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 4fed33eee3..d0161d839c 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.List; private byte[] scratchSpace; private IOException fatalError; private HlsUrl expectedPlaylistUrl; + private boolean independentSegments; private Uri encryptionKeyUri; private byte[] encryptionKey; @@ -206,10 +207,11 @@ import java.util.List; 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 - : Math.max(0, previous.startTimeUs - playbackPositionUs); + // Unless segments are known to be independent, switching variant will require downloading + // overlapping segments. Hence we use the start time of the previous chunk rather than its end + // time for this case. + long bufferedDurationUs = previous == null ? 0 : Math.max(0, + (independentSegments ? previous.endTimeUs : previous.startTimeUs) - playbackPositionUs); // Select the variant. trackSelection.updateSelectedTrack(bufferedDurationUs); @@ -224,12 +226,13 @@ import java.util.List; return; } HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); + independentSegments = mediaPlaylist.hasIndependentSegmentsTag; // Select the chunk. int chunkMediaSequence; if (previous == null || switchingVariant) { long targetPositionUs = previous == null ? playbackPositionUs - : mediaPlaylist.hasIndependentSegmentsTag ? previous.endTimeUs : previous.startTimeUs; + : independentSegments ? 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(); From f15ce81c4763e2a4e6c606c9aeb8d24024b86440 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 31 Aug 2017 09:33:55 -0700 Subject: [PATCH 0041/1327] Move some unit tests to use Robolectric ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167148146 --- constants.gradle | 3 + library/core/build.gradle | 7 + .../com/google/android/exoplayer2/CTest.java | 35 -- .../google/android/exoplayer2/FormatTest.java | 151 ------ .../exoplayer2/util/AtomicFileTest.java | 85 --- .../exoplayer2/util/ColorParserTest.java | 98 ---- .../exoplayer2/util/NalUnitUtilTest.java | 206 -------- .../exoplayer2/util/ParsableBitArrayTest.java | 177 ------- .../util/ParsableByteArrayTest.java | 492 ------------------ .../util/ParsableNalUnitBitArrayTest.java | 114 ---- .../ReusableBufferedOutputStreamTest.java | 46 -- .../android/exoplayer2/util/UriUtilTest.java | 98 ---- .../android/exoplayer2/util/UtilTest.java | 179 ------- 13 files changed, 10 insertions(+), 1681 deletions(-) delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/CTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/ColorParserTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/UriUtilTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java diff --git a/constants.gradle b/constants.gradle index 4107faab4c..dcec53efba 100644 --- a/constants.gradle +++ b/constants.gradle @@ -25,6 +25,9 @@ project.ext { playServicesLibraryVersion = '11.0.2' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' + junitVersion = '4.12' + truthVersion = '0.35' + robolectricVersion = '3.4.2' releaseVersion = 'r2.5.1' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { diff --git a/library/core/build.gradle b/library/core/build.gradle index ecad1e58b5..d50834efd5 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -28,6 +28,9 @@ android { androidTest { java.srcDirs += "../../testutils/src/main/java/" } + test { + java.srcDirs += "../../testutils/src/main/java/" + } } buildTypes { @@ -44,6 +47,10 @@ dependencies { androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion + testCompile 'com.google.truth:truth:' + truthVersion + testCompile 'junit:junit:' + junitVersion + testCompile 'org.mockito:mockito-core:' + mockitoVersion + testCompile 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/CTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/CTest.java deleted file mode 100644 index ddcdc4ac8a..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/CTest.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2; - -import android.annotation.SuppressLint; -import android.media.MediaCodec; -import junit.framework.TestCase; - -/** - * Unit test for {@link C}. - */ -public class CTest extends TestCase { - - @SuppressLint("InlinedApi") - public static void testConstants() { - // Sanity check that constant values match those defined by the platform. - assertEquals(MediaCodec.BUFFER_FLAG_KEY_FRAME, C.BUFFER_FLAG_KEY_FRAME); - assertEquals(MediaCodec.BUFFER_FLAG_END_OF_STREAM, C.BUFFER_FLAG_END_OF_STREAM); - assertEquals(MediaCodec.CRYPTO_MODE_AES_CTR, C.CRYPTO_MODE_AES_CTR); - } - -} 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 deleted file mode 100644 index bdea08638b..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2; - -import static com.google.android.exoplayer2.C.WIDEVINE_UUID; -import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_MP4; -import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_WEBM; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.media.MediaFormat; -import android.os.Parcel; -import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; -import com.google.android.exoplayer2.testutil.TestUtil; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; -import com.google.android.exoplayer2.video.ColorInfo; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import junit.framework.TestCase; - -/** - * Unit test for {@link Format}. - */ -public final class FormatTest extends TestCase { - - private static final List INIT_DATA; - static { - byte[] initData1 = new byte[] {1, 2, 3}; - byte[] initData2 = new byte[] {4, 5, 6}; - List initData = new ArrayList<>(); - initData.add(initData1); - initData.add(initData2); - INIT_DATA = Collections.unmodifiableList(initData); - } - - public void testParcelable() { - 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, 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}; - Metadata metadata = new Metadata( - new TextInformationFrame("id1", "description1", "value1"), - new TextInformationFrame("id2", "description2", "value2")); - ColorInfo colorInfo = new ColorInfo(C.COLOR_SPACE_BT709, - C.COLOR_RANGE_LIMITED, C.COLOR_TRANSFER_SDR, new byte[] {1, 2, 3, 4, 5, 6, 7}); - - Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, - 1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, colorInfo, 6, - 44100, C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.NO_VALUE, - Format.OFFSET_SAMPLE_RELATIVE, INIT_DATA, drmInitData, metadata); - - Parcel parcel = Parcel.obtain(); - formatToParcel.writeToParcel(parcel, 0); - parcel.setDataPosition(0); - - Format formatFromParcel = Format.CREATOR.createFromParcel(parcel); - assertEquals(formatToParcel, formatFromParcel); - - parcel.recycle(); - } - - public void testConversionToFrameworkMediaFormat() { - if (Util.SDK_INT < 16) { - // Test doesn't apply. - return; - } - - testConversionToFrameworkMediaFormatV16(Format.createVideoSampleFormat(null, "video/xyz", null, - 5000, 102400, 1280, 720, 30, INIT_DATA, null)); - testConversionToFrameworkMediaFormatV16(Format.createVideoSampleFormat(null, "video/xyz", null, - 5000, Format.NO_VALUE, 1280, 720, 30, null, null)); - testConversionToFrameworkMediaFormatV16(Format.createAudioSampleFormat(null, "audio/xyz", null, - 500, 128, 5, 44100, INIT_DATA, null, 0, null)); - testConversionToFrameworkMediaFormatV16(Format.createAudioSampleFormat(null, "audio/xyz", null, - 500, Format.NO_VALUE, 5, 44100, null, null, 0, null)); - testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", 0, - "eng")); - testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", 0, - null)); - } - - @SuppressLint("InlinedApi") - @TargetApi(16) - private static void testConversionToFrameworkMediaFormatV16(Format in) { - MediaFormat out = in.getFrameworkMediaFormatV16(); - assertEquals(in.sampleMimeType, out.getString(MediaFormat.KEY_MIME)); - assertOptionalV16(out, MediaFormat.KEY_LANGUAGE, in.language); - assertOptionalV16(out, MediaFormat.KEY_MAX_INPUT_SIZE, in.maxInputSize); - assertOptionalV16(out, MediaFormat.KEY_WIDTH, in.width); - assertOptionalV16(out, MediaFormat.KEY_HEIGHT, in.height); - assertOptionalV16(out, MediaFormat.KEY_CHANNEL_COUNT, in.channelCount); - assertOptionalV16(out, MediaFormat.KEY_SAMPLE_RATE, in.sampleRate); - assertOptionalV16(out, MediaFormat.KEY_FRAME_RATE, in.frameRate); - - for (int i = 0; i < in.initializationData.size(); i++) { - byte[] originalData = in.initializationData.get(i); - ByteBuffer frameworkBuffer = out.getByteBuffer("csd-" + i); - byte[] frameworkData = Arrays.copyOf(frameworkBuffer.array(), frameworkBuffer.limit()); - assertTrue(Arrays.equals(originalData, frameworkData)); - } - } - - @TargetApi(16) - private static void assertOptionalV16(MediaFormat format, String key, String value) { - if (value == null) { - assertFalse(format.containsKey(key)); - } else { - assertEquals(value, format.getString(key)); - } - } - - @TargetApi(16) - private static void assertOptionalV16(MediaFormat format, String key, int value) { - if (value == Format.NO_VALUE) { - assertFalse(format.containsKey(key)); - } else { - assertEquals(value, format.getInteger(key)); - } - } - - @TargetApi(16) - private static void assertOptionalV16(MediaFormat format, String key, float value) { - if (value == Format.NO_VALUE) { - assertFalse(format.containsKey(key)); - } else { - assertEquals(value, format.getFloat(key)); - } - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java deleted file mode 100644 index b4f1d50293..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.util; - -import android.test.InstrumentationTestCase; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -/** - * Tests {@link AtomicFile}. - */ -public class AtomicFileTest extends InstrumentationTestCase { - - private File tempFolder; - private File file; - private AtomicFile atomicFile; - - @Override - public void setUp() throws Exception { - tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); - file = new File(tempFolder, "atomicFile"); - atomicFile = new AtomicFile(file); - } - - @Override - protected void tearDown() throws Exception { - Util.recursiveDelete(tempFolder); - } - - public void testDelete() throws Exception { - assertTrue(file.createNewFile()); - atomicFile.delete(); - assertFalse(file.exists()); - } - - public void testWriteRead() throws Exception { - OutputStream output = atomicFile.startWrite(); - output.write(5); - atomicFile.endWrite(output); - output.close(); - - assertRead(); - - output = atomicFile.startWrite(); - output.write(5); - output.write(6); - output.close(); - - assertRead(); - - output = atomicFile.startWrite(); - output.write(6); - - assertRead(); - output.close(); - - output = atomicFile.startWrite(); - - assertRead(); - output.close(); - } - - private void assertRead() throws IOException { - InputStream input = atomicFile.openRead(); - assertEquals(5, input.read()); - assertEquals(-1, input.read()); - input.close(); - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ColorParserTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ColorParserTest.java deleted file mode 100644 index 641b58b0ce..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ColorParserTest.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.util; - -import android.graphics.Color; -import android.test.InstrumentationTestCase; - -/** - * Unit test for ColorParser. - */ -public class ColorParserTest extends InstrumentationTestCase { - - // Negative tests. - - public void testParseUnknownColor() { - try { - ColorParser.parseTtmlColor("colorOfAnElectron"); - fail(); - } catch (IllegalArgumentException e) { - // expected - } - } - - public void testParseNull() { - try { - ColorParser.parseTtmlColor(null); - fail(); - } catch (IllegalArgumentException e) { - // expected - } - } - - public void testParseEmpty() { - try { - ColorParser.parseTtmlColor(""); - fail(); - } catch (IllegalArgumentException e) { - // expected - } - } - - public void testRgbColorParsingRgbValuesNegative() { - try { - ColorParser.parseTtmlColor("rgb(-4, 55, 209)"); - fail(); - } catch (IllegalArgumentException e) { - // expected - } - } - - // Positive tests. - - public void testHexCodeParsing() { - assertEquals(Color.WHITE, ColorParser.parseTtmlColor("#FFFFFF")); - assertEquals(Color.WHITE, ColorParser.parseTtmlColor("#FFFFFFFF")); - assertEquals(Color.parseColor("#FF123456"), ColorParser.parseTtmlColor("#123456")); - // Hex colors in ColorParser are RGBA, where-as {@link Color#parseColor} takes ARGB. - assertEquals(Color.parseColor("#00FFFFFF"), ColorParser.parseTtmlColor("#FFFFFF00")); - assertEquals(Color.parseColor("#78123456"), ColorParser.parseTtmlColor("#12345678")); - } - - public void testRgbColorParsing() { - assertEquals(Color.WHITE, ColorParser.parseTtmlColor("rgb(255,255,255)")); - // Spaces are ignored. - assertEquals(Color.WHITE, ColorParser.parseTtmlColor(" rgb ( 255, 255, 255)")); - } - - public void testRgbColorParsingRgbValuesOutOfBounds() { - int outOfBounds = ColorParser.parseTtmlColor("rgb(999, 999, 999)"); - int color = Color.rgb(999, 999, 999); - // Behave like the framework does. - assertEquals(color, outOfBounds); - } - - public void testRgbaColorParsing() { - assertEquals(Color.WHITE, ColorParser.parseTtmlColor("rgba(255,255,255,255)")); - assertEquals(Color.argb(255, 255, 255, 255), - ColorParser.parseTtmlColor("rgba(255,255,255,255)")); - assertEquals(Color.BLACK, ColorParser.parseTtmlColor("rgba(0, 0, 0, 255)")); - assertEquals(Color.argb(0, 0, 0, 255), ColorParser.parseTtmlColor("rgba(0, 0, 255, 0)")); - assertEquals(Color.RED, ColorParser.parseTtmlColor("rgba(255, 0, 0, 255)")); - assertEquals(Color.argb(0, 255, 0, 255), ColorParser.parseTtmlColor("rgba(255, 0, 255, 0)")); - assertEquals(Color.argb(205, 255, 0, 0), ColorParser.parseTtmlColor("rgba(255, 0, 0, 205)")); - } -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java deleted file mode 100644 index 286013e83a..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.util; - -import static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; - -import java.nio.ByteBuffer; -import java.util.Arrays; -import junit.framework.TestCase; - -/** - * Tests for {@link NalUnitUtil}. - */ -public class NalUnitUtilTest extends TestCase { - - private static final int TEST_PARTIAL_NAL_POSITION = 4; - private static final int TEST_NAL_POSITION = 10; - private static final byte[] SPS_TEST_DATA = createByteArray(0x00, 0x00, 0x01, 0x67, 0x4D, 0x40, - 0x16, 0xEC, 0xA0, 0x50, 0x17, 0xFC, 0xB8, 0x08, 0x80, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, - 0x00, 0x0F, 0x47, 0x8B, 0x16, 0xCB); - private static final int SPS_TEST_DATA_OFFSET = 3; - - public void testFindNalUnit() { - byte[] data = buildTestData(); - - // Should find NAL unit. - int result = NalUnitUtil.findNalUnit(data, 0, data.length, null); - assertEquals(TEST_NAL_POSITION, result); - // Should find NAL unit whose prefix ends one byte before the limit. - result = NalUnitUtil.findNalUnit(data, 0, TEST_NAL_POSITION + 4, null); - assertEquals(TEST_NAL_POSITION, result); - // Shouldn't find NAL unit whose prefix ends at the limit (since the limit is exclusive). - result = NalUnitUtil.findNalUnit(data, 0, TEST_NAL_POSITION + 3, null); - assertEquals(TEST_NAL_POSITION + 3, result); - // Should find NAL unit whose prefix starts at the offset. - result = NalUnitUtil.findNalUnit(data, TEST_NAL_POSITION, data.length, null); - assertEquals(TEST_NAL_POSITION, result); - // Shouldn't find NAL unit whose prefix starts one byte past the offset. - result = NalUnitUtil.findNalUnit(data, TEST_NAL_POSITION + 1, data.length, null); - assertEquals(data.length, result); - } - - public void testFindNalUnitWithPrefix() { - byte[] data = buildTestData(); - - // First byte of NAL unit in data1, rest in data2. - boolean[] prefixFlags = new boolean[3]; - byte[] data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1); - byte[] data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, data.length); - int result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); - assertEquals(data1.length, result); - result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); - assertEquals(-1, result); - assertPrefixFlagsCleared(prefixFlags); - - // First three bytes of NAL unit in data1, rest in data2. - prefixFlags = new boolean[3]; - data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 3); - data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 3, data.length); - result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); - assertEquals(data1.length, result); - result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); - assertEquals(-3, result); - assertPrefixFlagsCleared(prefixFlags); - - // First byte of NAL unit in data1, second byte in data2, rest in data3. - prefixFlags = new boolean[3]; - data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1); - data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2); - byte[] data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length); - result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); - assertEquals(data1.length, result); - result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); - assertEquals(data2.length, result); - result = NalUnitUtil.findNalUnit(data3, 0, data3.length, prefixFlags); - assertEquals(-2, result); - assertPrefixFlagsCleared(prefixFlags); - - // NAL unit split with one byte in four arrays. - prefixFlags = new boolean[3]; - data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1); - data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2); - data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, TEST_NAL_POSITION + 3); - byte[] data4 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length); - result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); - assertEquals(data1.length, result); - result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); - assertEquals(data2.length, result); - result = NalUnitUtil.findNalUnit(data3, 0, data3.length, prefixFlags); - assertEquals(data3.length, result); - result = NalUnitUtil.findNalUnit(data4, 0, data4.length, prefixFlags); - assertEquals(-3, result); - assertPrefixFlagsCleared(prefixFlags); - - // NAL unit entirely in data2. data1 ends with partial prefix. - prefixFlags = new boolean[3]; - data1 = Arrays.copyOfRange(data, 0, TEST_PARTIAL_NAL_POSITION + 2); - data2 = Arrays.copyOfRange(data, TEST_PARTIAL_NAL_POSITION + 2, data.length); - result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); - assertEquals(data1.length, result); - result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); - assertEquals(4, result); - assertPrefixFlagsCleared(prefixFlags); - } - - public void testParseSpsNalUnit() { - NalUnitUtil.SpsData data = NalUnitUtil.parseSpsNalUnit(SPS_TEST_DATA, SPS_TEST_DATA_OFFSET, - SPS_TEST_DATA.length); - assertEquals(640, data.width); - assertEquals(360, data.height); - assertFalse(data.deltaPicOrderAlwaysZeroFlag); - assertTrue(data.frameMbsOnlyFlag); - assertEquals(4, data.frameNumLength); - assertEquals(6, data.picOrderCntLsbLength); - assertEquals(0, data.seqParameterSetId); - assertEquals(1.0f, data.pixelWidthAspectRatio); - assertEquals(0, data.picOrderCountType); - assertFalse(data.separateColorPlaneFlag); - } - - public void testUnescapeDoesNotModifyBuffersWithoutStartCodes() { - assertUnescapeDoesNotModify(""); - assertUnescapeDoesNotModify("0000"); - assertUnescapeDoesNotModify("172BF38A3C"); - assertUnescapeDoesNotModify("000004"); - } - - public void testUnescapeModifiesBuffersWithStartCodes() { - assertUnescapeMatchesExpected("00000301", "000001"); - assertUnescapeMatchesExpected("0000030200000300", "000002000000"); - } - - public void testDiscardToSps() { - assertDiscardToSpsMatchesExpected("", ""); - assertDiscardToSpsMatchesExpected("00", ""); - assertDiscardToSpsMatchesExpected("FFFF000001", ""); - assertDiscardToSpsMatchesExpected("00000001", ""); - assertDiscardToSpsMatchesExpected("00000001FF67", ""); - assertDiscardToSpsMatchesExpected("00000001000167", ""); - assertDiscardToSpsMatchesExpected("0000000167", "0000000167"); - assertDiscardToSpsMatchesExpected("0000000167FF", "0000000167FF"); - assertDiscardToSpsMatchesExpected("0000000167FF", "0000000167FF"); - assertDiscardToSpsMatchesExpected("0000000167FF000000016700", "0000000167FF000000016700"); - assertDiscardToSpsMatchesExpected("000000000167FF", "0000000167FF"); - assertDiscardToSpsMatchesExpected("0001670000000167FF", "0000000167FF"); - assertDiscardToSpsMatchesExpected("FF00000001660000000167FF", "0000000167FF"); - } - - private static byte[] buildTestData() { - byte[] data = new byte[20]; - for (int i = 0; i < data.length; i++) { - data[i] = (byte) 0xFF; - } - // Insert an incomplete NAL unit start code. - data[TEST_PARTIAL_NAL_POSITION] = 0; - data[TEST_PARTIAL_NAL_POSITION + 1] = 0; - // Insert a complete NAL unit start code. - data[TEST_NAL_POSITION] = 0; - data[TEST_NAL_POSITION + 1] = 0; - data[TEST_NAL_POSITION + 2] = 1; - data[TEST_NAL_POSITION + 3] = 5; - return data; - } - - private static void assertPrefixFlagsCleared(boolean[] flags) { - assertEquals(false, flags[0] || flags[1] || flags[2]); - } - - private static void assertUnescapeDoesNotModify(String input) { - assertUnescapeMatchesExpected(input, input); - } - - private static void assertUnescapeMatchesExpected(String input, String expectedOutput) { - byte[] bitstream = Util.getBytesFromHexString(input); - byte[] expectedOutputBitstream = Util.getBytesFromHexString(expectedOutput); - int count = NalUnitUtil.unescapeStream(bitstream, bitstream.length); - assertEquals(expectedOutputBitstream.length, count); - byte[] outputBitstream = new byte[count]; - System.arraycopy(bitstream, 0, outputBitstream, 0, count); - assertTrue(Arrays.equals(expectedOutputBitstream, outputBitstream)); - } - - private static void assertDiscardToSpsMatchesExpected(String input, String expectedOutput) { - byte[] bitstream = Util.getBytesFromHexString(input); - byte[] expectedOutputBitstream = Util.getBytesFromHexString(expectedOutput); - ByteBuffer buffer = ByteBuffer.wrap(bitstream); - buffer.position(buffer.limit()); - NalUnitUtil.discardToSps(buffer); - assertTrue(Arrays.equals(expectedOutputBitstream, - Arrays.copyOf(buffer.array(), buffer.position()))); - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java deleted file mode 100644 index d7b2b36740..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java +++ /dev/null @@ -1,177 +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.util; - -import android.test.MoreAsserts; -import junit.framework.TestCase; - -/** - * Tests for {@link ParsableBitArray}. - */ -public final class ParsableBitArrayTest extends TestCase { - - private static final byte[] TEST_DATA = new byte[] {0x3C, (byte) 0xD2, (byte) 0x5F, (byte) 0x01, - (byte) 0xFF, (byte) 0x14, (byte) 0x60, (byte) 0x99}; - - private ParsableBitArray testArray; - - @Override - public void setUp() { - testArray = new ParsableBitArray(TEST_DATA); - } - - public void testReadAllBytes() { - byte[] bytesRead = new byte[TEST_DATA.length]; - testArray.readBytes(bytesRead, 0, TEST_DATA.length); - MoreAsserts.assertEquals(TEST_DATA, bytesRead); - assertEquals(TEST_DATA.length * 8, testArray.getPosition()); - assertEquals(TEST_DATA.length, testArray.getBytePosition()); - } - - public void testReadBit() { - assertReadBitsToEnd(0); - } - - public void testReadBits() { - assertEquals(getTestDataBits(0, 5), testArray.readBits(5)); - assertEquals(getTestDataBits(5, 0), testArray.readBits(0)); - assertEquals(getTestDataBits(5, 3), testArray.readBits(3)); - assertEquals(getTestDataBits(8, 16), testArray.readBits(16)); - assertEquals(getTestDataBits(24, 3), testArray.readBits(3)); - assertEquals(getTestDataBits(27, 18), testArray.readBits(18)); - assertEquals(getTestDataBits(45, 5), testArray.readBits(5)); - assertEquals(getTestDataBits(50, 14), testArray.readBits(14)); - } - - public void testReadBitsToByteArray() { - byte[] result = new byte[TEST_DATA.length]; - // Test read within byte boundaries. - testArray.readBits(result, 0, 6); - assertEquals(TEST_DATA[0] & 0xFC, result[0]); - // Test read across byte boundaries. - testArray.readBits(result, 0, 8); - assertEquals(((TEST_DATA[0] & 0x03) << 6) | ((TEST_DATA[1] & 0xFC) >> 2), result[0]); - // Test reading across multiple bytes. - testArray.readBits(result, 1, 50); - for (int i = 1; i < 7; i++) { - assertEquals((byte) (((TEST_DATA[i] & 0x03) << 6) | ((TEST_DATA[i + 1] & 0xFC) >> 2)), - result[i]); - } - assertEquals((byte) (TEST_DATA[7] & 0x03) << 6, result[7]); - assertEquals(0, testArray.bitsLeft()); - // Test read last buffer byte across input data bytes. - testArray.setPosition(31); - result[3] = 0; - testArray.readBits(result, 3, 3); - assertEquals((byte) 0xE0, result[3]); - // Test read bits in the middle of a input data byte. - result[0] = 0; - assertEquals(34, testArray.getPosition()); - testArray.readBits(result, 0, 3); - assertEquals((byte) 0xE0, result[0]); - // Test read 0 bits. - testArray.setPosition(32); - result[1] = 0; - testArray.readBits(result, 1, 0); - assertEquals(0, result[1]); - // Test reading a number of bits divisible by 8. - testArray.setPosition(0); - testArray.readBits(result, 0, 16); - assertEquals(TEST_DATA[0], result[0]); - assertEquals(TEST_DATA[1], result[1]); - // Test least significant bits are unmodified. - result[1] = (byte) 0xFF; - testArray.readBits(result, 0, 9); - assertEquals(0x5F, result[0]); - assertEquals(0x7F, result[1]); - } - - public void testRead32BitsByteAligned() { - assertEquals(getTestDataBits(0, 32), testArray.readBits(32)); - assertEquals(getTestDataBits(32, 32), testArray.readBits(32)); - } - - public void testRead32BitsNonByteAligned() { - assertEquals(getTestDataBits(0, 5), testArray.readBits(5)); - assertEquals(getTestDataBits(5, 32), testArray.readBits(32)); - } - - public void testSkipBytes() { - testArray.skipBytes(2); - assertReadBitsToEnd(16); - } - - public void testSkipBitsByteAligned() { - testArray.skipBits(16); - assertReadBitsToEnd(16); - } - - public void testSkipBitsNonByteAligned() { - testArray.skipBits(5); - assertReadBitsToEnd(5); - } - - public void testSetPositionByteAligned() { - testArray.setPosition(16); - assertReadBitsToEnd(16); - } - - public void testSetPositionNonByteAligned() { - testArray.setPosition(5); - assertReadBitsToEnd(5); - } - - public void testByteAlignFromNonByteAligned() { - testArray.setPosition(11); - testArray.byteAlign(); - assertEquals(2, testArray.getBytePosition()); - assertEquals(16, testArray.getPosition()); - assertReadBitsToEnd(16); - } - - public void testByteAlignFromByteAligned() { - testArray.setPosition(16); - testArray.byteAlign(); // Should be a no-op. - assertEquals(2, testArray.getBytePosition()); - assertEquals(16, testArray.getPosition()); - assertReadBitsToEnd(16); - } - - private void assertReadBitsToEnd(int expectedStartPosition) { - int position = testArray.getPosition(); - assertEquals(expectedStartPosition, position); - for (int i = position; i < TEST_DATA.length * 8; i++) { - assertEquals(getTestDataBit(i), testArray.readBit()); - assertEquals(i + 1, testArray.getPosition()); - } - } - - private static int getTestDataBits(int bitPosition, int length) { - int result = 0; - for (int i = 0; i < length; i++) { - result = result << 1; - if (getTestDataBit(bitPosition++)) { - result |= 0x1; - } - } - return result; - } - - private static boolean getTestDataBit(int bitPosition) { - return (TEST_DATA[bitPosition / 8] & (0x80 >>> (bitPosition % 8))) != 0; - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java deleted file mode 100644 index 324d668c7a..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java +++ /dev/null @@ -1,492 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.util; - -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.Arrays; -import junit.framework.TestCase; - -/** - * Tests for {@link ParsableByteArray}. - */ -public class ParsableByteArrayTest extends TestCase { - - private static final byte[] TEST_DATA = - new byte[] {0x0F, (byte) 0xFF, (byte) 0x42, (byte) 0x0F, 0x00, 0x00, 0x00, 0x00}; - - private static ParsableByteArray getTestDataArray() { - ParsableByteArray testArray = new ParsableByteArray(TEST_DATA.length); - System.arraycopy(TEST_DATA, 0, testArray.data, 0, TEST_DATA.length); - return testArray; - } - - public void testReadShort() { - testReadShort((short) -1); - testReadShort((short) 0); - testReadShort((short) 1); - testReadShort(Short.MIN_VALUE); - testReadShort(Short.MAX_VALUE); - } - - private static void testReadShort(short testValue) { - ParsableByteArray testArray = new ParsableByteArray( - ByteBuffer.allocate(4).putShort(testValue).array()); - int readValue = testArray.readShort(); - - // Assert that the value we read was the value we wrote. - assertEquals(testValue, readValue); - // And that the position advanced as expected. - assertEquals(2, testArray.getPosition()); - - // And that skipping back and reading gives the same results. - testArray.skipBytes(-2); - readValue = testArray.readShort(); - assertEquals(testValue, readValue); - assertEquals(2, testArray.getPosition()); - } - - public void testReadInt() { - testReadInt(0); - testReadInt(1); - testReadInt(-1); - testReadInt(Integer.MIN_VALUE); - testReadInt(Integer.MAX_VALUE); - } - - private static void testReadInt(int testValue) { - ParsableByteArray testArray = new ParsableByteArray( - ByteBuffer.allocate(4).putInt(testValue).array()); - int readValue = testArray.readInt(); - - // Assert that the value we read was the value we wrote. - assertEquals(testValue, readValue); - // And that the position advanced as expected. - assertEquals(4, testArray.getPosition()); - - // And that skipping back and reading gives the same results. - testArray.skipBytes(-4); - readValue = testArray.readInt(); - assertEquals(testValue, readValue); - assertEquals(4, testArray.getPosition()); - } - - public void testReadUnsignedInt() { - testReadUnsignedInt(0); - testReadUnsignedInt(1); - testReadUnsignedInt(Integer.MAX_VALUE); - testReadUnsignedInt(Integer.MAX_VALUE + 1L); - testReadUnsignedInt(0xFFFFFFFFL); - } - - private static void testReadUnsignedInt(long testValue) { - ParsableByteArray testArray = new ParsableByteArray( - Arrays.copyOfRange(ByteBuffer.allocate(8).putLong(testValue).array(), 4, 8)); - long readValue = testArray.readUnsignedInt(); - - // Assert that the value we read was the value we wrote. - assertEquals(testValue, readValue); - // And that the position advanced as expected. - assertEquals(4, testArray.getPosition()); - - // And that skipping back and reading gives the same results. - testArray.skipBytes(-4); - readValue = testArray.readUnsignedInt(); - assertEquals(testValue, readValue); - assertEquals(4, testArray.getPosition()); - } - - public void testReadUnsignedIntToInt() { - testReadUnsignedIntToInt(0); - testReadUnsignedIntToInt(1); - testReadUnsignedIntToInt(Integer.MAX_VALUE); - try { - testReadUnsignedIntToInt(-1); - fail(); - } catch (IllegalStateException e) { - // Expected. - } - try { - testReadUnsignedIntToInt(Integer.MIN_VALUE); - fail(); - } catch (IllegalStateException e) { - // Expected. - } - } - - private static void testReadUnsignedIntToInt(int testValue) { - ParsableByteArray testArray = new ParsableByteArray( - ByteBuffer.allocate(4).putInt(testValue).array()); - int readValue = testArray.readUnsignedIntToInt(); - - // Assert that the value we read was the value we wrote. - assertEquals(testValue, readValue); - // And that the position advanced as expected. - assertEquals(4, testArray.getPosition()); - - // And that skipping back and reading gives the same results. - testArray.skipBytes(-4); - readValue = testArray.readUnsignedIntToInt(); - assertEquals(testValue, readValue); - assertEquals(4, testArray.getPosition()); - } - - public void testReadUnsignedLongToLong() { - testReadUnsignedLongToLong(0); - testReadUnsignedLongToLong(1); - testReadUnsignedLongToLong(Long.MAX_VALUE); - try { - testReadUnsignedLongToLong(-1); - fail(); - } catch (IllegalStateException e) { - // Expected. - } - try { - testReadUnsignedLongToLong(Long.MIN_VALUE); - fail(); - } catch (IllegalStateException e) { - // Expected. - } - } - - private static void testReadUnsignedLongToLong(long testValue) { - ParsableByteArray testArray = new ParsableByteArray( - ByteBuffer.allocate(8).putLong(testValue).array()); - long readValue = testArray.readUnsignedLongToLong(); - - // Assert that the value we read was the value we wrote. - assertEquals(testValue, readValue); - // And that the position advanced as expected. - assertEquals(8, testArray.getPosition()); - - // And that skipping back and reading gives the same results. - testArray.skipBytes(-8); - readValue = testArray.readUnsignedLongToLong(); - assertEquals(testValue, readValue); - assertEquals(8, testArray.getPosition()); - } - - public void testReadLong() { - testReadLong(0); - testReadLong(1); - testReadLong(-1); - testReadLong(Long.MIN_VALUE); - testReadLong(Long.MAX_VALUE); - } - - private static void testReadLong(long testValue) { - ParsableByteArray testArray = new ParsableByteArray( - ByteBuffer.allocate(8).putLong(testValue).array()); - long readValue = testArray.readLong(); - - // Assert that the value we read was the value we wrote. - assertEquals(testValue, readValue); - // And that the position advanced as expected. - assertEquals(8, testArray.getPosition()); - - // And that skipping back and reading gives the same results. - testArray.skipBytes(-8); - readValue = testArray.readLong(); - assertEquals(testValue, readValue); - assertEquals(8, testArray.getPosition()); - } - - public void testReadingMovesPosition() { - ParsableByteArray parsableByteArray = getTestDataArray(); - - // Given an array at the start - assertEquals(0, parsableByteArray.getPosition()); - // When reading an integer, the position advances - parsableByteArray.readUnsignedInt(); - assertEquals(4, parsableByteArray.getPosition()); - } - - public void testOutOfBoundsThrows() { - ParsableByteArray parsableByteArray = getTestDataArray(); - - // Given an array at the end - parsableByteArray.readUnsignedLongToLong(); - assertEquals(TEST_DATA.length, parsableByteArray.getPosition()); - // Then reading more data throws. - try { - parsableByteArray.readUnsignedInt(); - fail(); - } catch (Exception e) { - // Expected. - } - } - - public void testModificationsAffectParsableArray() { - ParsableByteArray parsableByteArray = getTestDataArray(); - - // When modifying the wrapped byte array - byte[] data = parsableByteArray.data; - long readValue = parsableByteArray.readUnsignedInt(); - data[0] = (byte) (TEST_DATA[0] + 1); - parsableByteArray.setPosition(0); - // Then the parsed value changes. - assertFalse(parsableByteArray.readUnsignedInt() == readValue); - } - - public void testReadingUnsignedLongWithMsbSetThrows() { - ParsableByteArray parsableByteArray = getTestDataArray(); - - // Given an array with the most-significant bit set on the top byte - byte[] data = parsableByteArray.data; - data[0] = (byte) 0x80; - // Then reading an unsigned long throws. - try { - parsableByteArray.readUnsignedLongToLong(); - fail(); - } catch (Exception e) { - // Expected. - } - } - - public void testReadUnsignedFixedPoint1616() { - ParsableByteArray parsableByteArray = getTestDataArray(); - - // When reading the integer part of a 16.16 fixed point value - int value = parsableByteArray.readUnsignedFixedPoint1616(); - // Then the read value is equal to the array elements interpreted as a short. - assertEquals((0xFF & TEST_DATA[0]) << 8 | (TEST_DATA[1] & 0xFF), value); - assertEquals(4, parsableByteArray.getPosition()); - } - - public void testReadingBytesReturnsCopy() { - ParsableByteArray parsableByteArray = getTestDataArray(); - - // When reading all the bytes back - int length = parsableByteArray.limit(); - assertEquals(TEST_DATA.length, length); - byte[] copy = new byte[length]; - parsableByteArray.readBytes(copy, 0, length); - // Then the array elements are the same. - assertTrue(Arrays.equals(parsableByteArray.data, copy)); - } - - public void testReadLittleEndianLong() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[] { - 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, (byte) 0xFF - }); - assertEquals(0xFF00000000000001L, byteArray.readLittleEndianLong()); - assertEquals(8, byteArray.getPosition()); - } - - public void testReadLittleEndianUnsignedInt() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[] { - 0x10, 0x00, 0x00, (byte) 0xFF - }); - assertEquals(0xFF000010L, byteArray.readLittleEndianUnsignedInt()); - assertEquals(4, byteArray.getPosition()); - } - - public void testReadLittleEndianInt() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[] { - 0x01, 0x00, 0x00, (byte) 0xFF - }); - assertEquals(0xFF000001, byteArray.readLittleEndianInt()); - assertEquals(4, byteArray.getPosition()); - } - - public void testReadLittleEndianUnsignedInt24() { - byte[] data = { 0x01, 0x02, (byte) 0xFF }; - ParsableByteArray byteArray = new ParsableByteArray(data); - assertEquals(0xFF0201, byteArray.readLittleEndianUnsignedInt24()); - assertEquals(3, byteArray.getPosition()); - } - - public void testReadLittleEndianUnsignedShort() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[] { - 0x01, (byte) 0xFF, 0x02, (byte) 0xFF - }); - assertEquals(0xFF01, byteArray.readLittleEndianUnsignedShort()); - assertEquals(2, byteArray.getPosition()); - assertEquals(0xFF02, byteArray.readLittleEndianUnsignedShort()); - assertEquals(4, byteArray.getPosition()); - } - - public void testReadLittleEndianShort() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[] { - 0x01, (byte) 0xFF, 0x02, (byte) 0xFF - }); - assertEquals((short) 0xFF01, byteArray.readLittleEndianShort()); - assertEquals(2, byteArray.getPosition()); - assertEquals((short) 0xFF02, byteArray.readLittleEndianShort()); - assertEquals(4, byteArray.getPosition()); - } - - public void testReadString() { - byte[] data = { - (byte) 0xC3, (byte) 0xA4, (byte) 0x20, - (byte) 0xC3, (byte) 0xB6, (byte) 0x20, - (byte) 0xC2, (byte) 0xAE, (byte) 0x20, - (byte) 0xCF, (byte) 0x80, (byte) 0x20, - (byte) 0xE2, (byte) 0x88, (byte) 0x9A, (byte) 0x20, - (byte) 0xC2, (byte) 0xB1, (byte) 0x20, - (byte) 0xE8, (byte) 0xB0, (byte) 0xA2, (byte) 0x20, - }; - ParsableByteArray byteArray = new ParsableByteArray(data); - assertEquals("ä ö ® π √ ± 谢 ", byteArray.readString(data.length)); - assertEquals(data.length, byteArray.getPosition()); - } - - public void testReadAsciiString() { - byte[] data = new byte[] {'t', 'e', 's', 't'}; - ParsableByteArray testArray = new ParsableByteArray(data); - assertEquals("test", testArray.readString(data.length, Charset.forName("US-ASCII"))); - assertEquals(data.length, testArray.getPosition()); - } - - public void testReadStringOutOfBoundsDoesNotMovePosition() { - byte[] data = { - (byte) 0xC3, (byte) 0xA4, (byte) 0x20 - }; - ParsableByteArray byteArray = new ParsableByteArray(data); - try { - byteArray.readString(data.length + 1); - fail(); - } catch (StringIndexOutOfBoundsException e) { - assertEquals(0, byteArray.getPosition()); - } - } - - public void testReadEmptyString() { - byte[] bytes = new byte[0]; - ParsableByteArray parser = new ParsableByteArray(bytes); - assertNull(parser.readLine()); - } - - public void testReadNullTerminatedStringWithLengths() { - byte[] bytes = new byte[] { - 'f', 'o', 'o', 0, 'b', 'a', 'r', 0 - }; - // Test with lengths that match NUL byte positions. - ParsableByteArray parser = new ParsableByteArray(bytes); - assertEquals("foo", parser.readNullTerminatedString(4)); - assertEquals(4, parser.getPosition()); - assertEquals("bar", parser.readNullTerminatedString(4)); - assertEquals(8, parser.getPosition()); - assertNull(parser.readNullTerminatedString()); - // Test with lengths that do not match NUL byte positions. - parser = new ParsableByteArray(bytes); - assertEquals("fo", parser.readNullTerminatedString(2)); - assertEquals(2, parser.getPosition()); - assertEquals("o", parser.readNullTerminatedString(2)); - assertEquals(4, parser.getPosition()); - assertEquals("bar", parser.readNullTerminatedString(3)); - assertEquals(7, parser.getPosition()); - assertEquals("", parser.readNullTerminatedString(1)); - assertEquals(8, parser.getPosition()); - assertNull(parser.readNullTerminatedString()); - // Test with limit at NUL - parser = new ParsableByteArray(bytes, 4); - assertEquals("foo", parser.readNullTerminatedString(4)); - assertEquals(4, parser.getPosition()); - assertNull(parser.readNullTerminatedString()); - // Test with limit before NUL - parser = new ParsableByteArray(bytes, 3); - assertEquals("foo", parser.readNullTerminatedString(3)); - assertEquals(3, parser.getPosition()); - assertNull(parser.readNullTerminatedString()); - } - - public void testReadNullTerminatedString() { - byte[] bytes = new byte[] { - 'f', 'o', 'o', 0, 'b', 'a', 'r', 0 - }; - // Test normal case. - ParsableByteArray parser = new ParsableByteArray(bytes); - assertEquals("foo", parser.readNullTerminatedString()); - assertEquals(4, parser.getPosition()); - assertEquals("bar", parser.readNullTerminatedString()); - assertEquals(8, parser.getPosition()); - assertNull(parser.readNullTerminatedString()); - // Test with limit at NUL. - parser = new ParsableByteArray(bytes, 4); - assertEquals("foo", parser.readNullTerminatedString()); - assertEquals(4, parser.getPosition()); - assertNull(parser.readNullTerminatedString()); - // Test with limit before NUL. - parser = new ParsableByteArray(bytes, 3); - assertEquals("foo", parser.readNullTerminatedString()); - assertEquals(3, parser.getPosition()); - assertNull(parser.readNullTerminatedString()); - } - - public void testReadNullTerminatedStringWithoutEndingNull() { - byte[] bytes = new byte[] { - 'f', 'o', 'o', 0, 'b', 'a', 'r' - }; - ParsableByteArray parser = new ParsableByteArray(bytes); - assertEquals("foo", parser.readNullTerminatedString()); - assertEquals("bar", parser.readNullTerminatedString()); - assertNull(parser.readNullTerminatedString()); - } - - public void testReadSingleLineWithoutEndingTrail() { - byte[] bytes = new byte[] { - 'f', 'o', 'o' - }; - ParsableByteArray parser = new ParsableByteArray(bytes); - assertEquals("foo", parser.readLine()); - assertNull(parser.readLine()); - } - - public void testReadSingleLineWithEndingLf() { - byte[] bytes = new byte[] { - 'f', 'o', 'o', '\n' - }; - ParsableByteArray parser = new ParsableByteArray(bytes); - assertEquals("foo", parser.readLine()); - assertNull(parser.readLine()); - } - - public void testReadTwoLinesWithCrFollowedByLf() { - byte[] bytes = new byte[] { - 'f', 'o', 'o', '\r', '\n', 'b', 'a', 'r' - }; - ParsableByteArray parser = new ParsableByteArray(bytes); - assertEquals("foo", parser.readLine()); - assertEquals("bar", parser.readLine()); - assertNull(parser.readLine()); - } - - public void testReadThreeLinesWithEmptyLine() { - byte[] bytes = new byte[] { - 'f', 'o', 'o', '\r', '\n', '\r', 'b', 'a', 'r' - }; - ParsableByteArray parser = new ParsableByteArray(bytes); - assertEquals("foo", parser.readLine()); - assertEquals("", parser.readLine()); - assertEquals("bar", parser.readLine()); - assertNull(parser.readLine()); - } - - public void testReadFourLinesWithLfFollowedByCr() { - byte[] bytes = new byte[] { - 'f', 'o', 'o', '\n', '\r', '\r', 'b', 'a', 'r', '\r', '\n' - }; - ParsableByteArray parser = new ParsableByteArray(bytes); - assertEquals("foo", parser.readLine()); - assertEquals("", parser.readLine()); - assertEquals("", parser.readLine()); - assertEquals("bar", parser.readLine()); - assertNull(parser.readLine()); - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java deleted file mode 100644 index 294d3d352a..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.util; - -import static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; - -import junit.framework.TestCase; - -/** - * Tests for {@link ParsableNalUnitBitArray}. - */ -public final class ParsableNalUnitBitArrayTest extends TestCase { - - private static final byte[] NO_ESCAPING_TEST_DATA = createByteArray(0, 3, 0, 1, 3, 0, 0); - private static final byte[] ALL_ESCAPING_TEST_DATA = createByteArray(0, 0, 3, 0, 0, 3, 0, 0, 3); - private static final byte[] MIX_TEST_DATA = createByteArray(255, 0, 0, 3, 255, 0, 0, 127); - - public void testReadNoEscaping() { - ParsableNalUnitBitArray array = - new ParsableNalUnitBitArray(NO_ESCAPING_TEST_DATA, 0, NO_ESCAPING_TEST_DATA.length); - assertEquals(0x000300, array.readBits(24)); - assertEquals(0, array.readBits(7)); - assertTrue(array.readBit()); - assertEquals(0x030000, array.readBits(24)); - assertFalse(array.canReadBits(1)); - assertFalse(array.canReadBits(8)); - } - - public void testReadNoEscapingTruncated() { - ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(NO_ESCAPING_TEST_DATA, 0, 4); - assertTrue(array.canReadBits(32)); - array.skipBits(32); - assertFalse(array.canReadBits(1)); - try { - array.readBit(); - fail(); - } catch (Exception e) { - // Expected. - } - } - - public void testReadAllEscaping() { - ParsableNalUnitBitArray array = - new ParsableNalUnitBitArray(ALL_ESCAPING_TEST_DATA, 0, ALL_ESCAPING_TEST_DATA.length); - assertTrue(array.canReadBits(48)); - assertFalse(array.canReadBits(49)); - assertEquals(0, array.readBits(15)); - assertFalse(array.readBit()); - assertEquals(0, array.readBits(17)); - assertEquals(0, array.readBits(15)); - } - - public void testReadMix() { - ParsableNalUnitBitArray array = - new ParsableNalUnitBitArray(MIX_TEST_DATA, 0, MIX_TEST_DATA.length); - assertTrue(array.canReadBits(56)); - assertFalse(array.canReadBits(57)); - assertEquals(127, array.readBits(7)); - assertEquals(2, array.readBits(2)); - assertEquals(3, array.readBits(17)); - assertEquals(126, array.readBits(7)); - assertEquals(127, array.readBits(23)); - assertFalse(array.canReadBits(1)); - } - - public void testReadExpGolomb() { - ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(createByteArray(0x9E), 0, 1); - assertTrue(array.canReadExpGolombCodedNum()); - assertEquals(0, array.readUnsignedExpGolombCodedInt()); - assertEquals(6, array.readUnsignedExpGolombCodedInt()); - assertEquals(0, array.readUnsignedExpGolombCodedInt()); - assertFalse(array.canReadExpGolombCodedNum()); - try { - array.readUnsignedExpGolombCodedInt(); - fail(); - } catch (Exception e) { - // Expected. - } - } - - public void testReadExpGolombWithEscaping() { - ParsableNalUnitBitArray array = - new ParsableNalUnitBitArray(createByteArray(0, 0, 3, 128, 0), 0, 5); - assertFalse(array.canReadExpGolombCodedNum()); - array.skipBit(); - assertTrue(array.canReadExpGolombCodedNum()); - assertEquals(32767, array.readUnsignedExpGolombCodedInt()); - assertFalse(array.canReadBits(1)); - } - - public void testReset() { - ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(createByteArray(0, 0), 0, 2); - assertFalse(array.canReadExpGolombCodedNum()); - assertTrue(array.canReadBits(16)); - assertFalse(array.canReadBits(17)); - array.reset(createByteArray(0, 0, 3, 0), 0, 4); - assertTrue(array.canReadBits(24)); - assertFalse(array.canReadBits(25)); - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java deleted file mode 100644 index beb9e44853..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.util; - -import android.test.MoreAsserts; -import java.io.ByteArrayOutputStream; -import junit.framework.TestCase; - -/** - * Tests {@link ReusableBufferedOutputStream}. - */ -public class ReusableBufferedOutputStreamTest extends TestCase { - - private static final byte[] TEST_DATA_1 = "test data 1".getBytes(); - private static final byte[] TEST_DATA_2 = "2 test data".getBytes(); - - public void testReset() throws Exception { - ByteArrayOutputStream byteArrayOutputStream1 = new ByteArrayOutputStream(1000); - ReusableBufferedOutputStream outputStream = new ReusableBufferedOutputStream( - byteArrayOutputStream1, 1000); - outputStream.write(TEST_DATA_1); - outputStream.close(); - - ByteArrayOutputStream byteArrayOutputStream2 = new ByteArrayOutputStream(1000); - outputStream.reset(byteArrayOutputStream2); - outputStream.write(TEST_DATA_2); - outputStream.close(); - - MoreAsserts.assertEquals(TEST_DATA_1, byteArrayOutputStream1.toByteArray()); - MoreAsserts.assertEquals(TEST_DATA_2, byteArrayOutputStream2.toByteArray()); - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UriUtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UriUtilTest.java deleted file mode 100644 index 1755c6f70d..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UriUtilTest.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.util; - -import junit.framework.TestCase; - -/** - * Unit tests for {@link UriUtil}. - */ -public class UriUtilTest extends TestCase { - - /** - * Tests normal usage of {@link UriUtil#resolve(String, String)}. - *

    - * The test cases are taken from RFC-3986 5.4.1. - */ - public void testResolveNormal() { - String base = "http://a/b/c/d;p?q"; - - assertEquals("g:h", UriUtil.resolve(base, "g:h")); - assertEquals("http://a/b/c/g", UriUtil.resolve(base, "g")); - assertEquals("http://a/b/c/g/", UriUtil.resolve(base, "g/")); - assertEquals("http://a/g", UriUtil.resolve(base, "/g")); - assertEquals("http://g", UriUtil.resolve(base, "//g")); - assertEquals("http://a/b/c/d;p?y", UriUtil.resolve(base, "?y")); - assertEquals("http://a/b/c/g?y", UriUtil.resolve(base, "g?y")); - assertEquals("http://a/b/c/d;p?q#s", UriUtil.resolve(base, "#s")); - assertEquals("http://a/b/c/g#s", UriUtil.resolve(base, "g#s")); - assertEquals("http://a/b/c/g?y#s", UriUtil.resolve(base, "g?y#s")); - assertEquals("http://a/b/c/;x", UriUtil.resolve(base, ";x")); - assertEquals("http://a/b/c/g;x", UriUtil.resolve(base, "g;x")); - assertEquals("http://a/b/c/g;x?y#s", UriUtil.resolve(base, "g;x?y#s")); - assertEquals("http://a/b/c/d;p?q", UriUtil.resolve(base, "")); - assertEquals("http://a/b/c/", UriUtil.resolve(base, ".")); - assertEquals("http://a/b/c/", UriUtil.resolve(base, "./")); - assertEquals("http://a/b/", UriUtil.resolve(base, "..")); - assertEquals("http://a/b/", UriUtil.resolve(base, "../")); - assertEquals("http://a/b/g", UriUtil.resolve(base, "../g")); - assertEquals("http://a/", UriUtil.resolve(base, "../..")); - assertEquals("http://a/", UriUtil.resolve(base, "../../")); - assertEquals("http://a/g", UriUtil.resolve(base, "../../g")); - } - - /** - * Tests abnormal usage of {@link UriUtil#resolve(String, String)}. - *

    - * The test cases are taken from RFC-3986 5.4.2. - */ - public void testResolveAbnormal() { - String base = "http://a/b/c/d;p?q"; - - assertEquals("http://a/g", UriUtil.resolve(base, "../../../g")); - assertEquals("http://a/g", UriUtil.resolve(base, "../../../../g")); - - assertEquals("http://a/g", UriUtil.resolve(base, "/./g")); - assertEquals("http://a/g", UriUtil.resolve(base, "/../g")); - assertEquals("http://a/b/c/g.", UriUtil.resolve(base, "g.")); - assertEquals("http://a/b/c/.g", UriUtil.resolve(base, ".g")); - assertEquals("http://a/b/c/g..", UriUtil.resolve(base, "g..")); - assertEquals("http://a/b/c/..g", UriUtil.resolve(base, "..g")); - - assertEquals("http://a/b/g", UriUtil.resolve(base, "./../g")); - assertEquals("http://a/b/c/g/", UriUtil.resolve(base, "./g/.")); - assertEquals("http://a/b/c/g/h", UriUtil.resolve(base, "g/./h")); - assertEquals("http://a/b/c/h", UriUtil.resolve(base, "g/../h")); - assertEquals("http://a/b/c/g;x=1/y", UriUtil.resolve(base, "g;x=1/./y")); - assertEquals("http://a/b/c/y", UriUtil.resolve(base, "g;x=1/../y")); - - assertEquals("http://a/b/c/g?y/./x", UriUtil.resolve(base, "g?y/./x")); - assertEquals("http://a/b/c/g?y/../x", UriUtil.resolve(base, "g?y/../x")); - assertEquals("http://a/b/c/g#s/./x", UriUtil.resolve(base, "g#s/./x")); - assertEquals("http://a/b/c/g#s/../x", UriUtil.resolve(base, "g#s/../x")); - - assertEquals("http:g", UriUtil.resolve(base, "http:g")); - } - - /** - * Tests additional abnormal usage of {@link UriUtil#resolve(String, String)}. - */ - public void testResolveAbnormalAdditional() { - assertEquals("c:e", UriUtil.resolve("http://a/b", "c:d/../e")); - assertEquals("a:c", UriUtil.resolve("a:b", "../c")); - } - -} 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 deleted file mode 100644 index 1d9aff0723..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.util; - -import com.google.android.exoplayer2.testutil.TestUtil; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import junit.framework.TestCase; - -/** - * Unit tests for {@link Util}. - */ -public class UtilTest extends TestCase { - - public void testArrayBinarySearchFloor() { - long[] values = new long[0]; - assertEquals(-1, Util.binarySearchFloor(values, 0, false, false)); - assertEquals(0, Util.binarySearchFloor(values, 0, false, true)); - - values = new long[] {1, 3, 5}; - assertEquals(-1, Util.binarySearchFloor(values, 0, false, false)); - assertEquals(-1, Util.binarySearchFloor(values, 0, true, false)); - assertEquals(0, Util.binarySearchFloor(values, 0, false, true)); - assertEquals(0, Util.binarySearchFloor(values, 0, true, true)); - - assertEquals(-1, Util.binarySearchFloor(values, 1, false, false)); - assertEquals(0, Util.binarySearchFloor(values, 1, true, false)); - assertEquals(0, Util.binarySearchFloor(values, 1, false, true)); - assertEquals(0, Util.binarySearchFloor(values, 1, true, true)); - - assertEquals(1, Util.binarySearchFloor(values, 4, false, false)); - assertEquals(1, Util.binarySearchFloor(values, 4, true, false)); - - assertEquals(1, Util.binarySearchFloor(values, 5, false, false)); - assertEquals(2, Util.binarySearchFloor(values, 5, true, false)); - - assertEquals(2, Util.binarySearchFloor(values, 6, false, false)); - assertEquals(2, Util.binarySearchFloor(values, 6, true, false)); - } - - public void testListBinarySearchFloor() { - List values = new ArrayList<>(); - assertEquals(-1, Util.binarySearchFloor(values, 0, false, false)); - assertEquals(0, Util.binarySearchFloor(values, 0, false, true)); - - values.add(1); - values.add(3); - values.add(5); - assertEquals(-1, Util.binarySearchFloor(values, 0, false, false)); - assertEquals(-1, Util.binarySearchFloor(values, 0, true, false)); - assertEquals(0, Util.binarySearchFloor(values, 0, false, true)); - assertEquals(0, Util.binarySearchFloor(values, 0, true, true)); - - assertEquals(-1, Util.binarySearchFloor(values, 1, false, false)); - assertEquals(0, Util.binarySearchFloor(values, 1, true, false)); - assertEquals(0, Util.binarySearchFloor(values, 1, false, true)); - assertEquals(0, Util.binarySearchFloor(values, 1, true, true)); - - assertEquals(1, Util.binarySearchFloor(values, 4, false, false)); - assertEquals(1, Util.binarySearchFloor(values, 4, true, false)); - - assertEquals(1, Util.binarySearchFloor(values, 5, false, false)); - assertEquals(2, Util.binarySearchFloor(values, 5, true, false)); - - assertEquals(2, Util.binarySearchFloor(values, 6, false, false)); - assertEquals(2, Util.binarySearchFloor(values, 6, true, false)); - } - - public void testArrayBinarySearchCeil() { - long[] values = new long[0]; - assertEquals(0, Util.binarySearchCeil(values, 0, false, false)); - assertEquals(-1, Util.binarySearchCeil(values, 0, false, true)); - - values = new long[] {1, 3, 5}; - assertEquals(0, Util.binarySearchCeil(values, 0, false, false)); - assertEquals(0, Util.binarySearchCeil(values, 0, true, false)); - - assertEquals(1, Util.binarySearchCeil(values, 1, false, false)); - assertEquals(0, Util.binarySearchCeil(values, 1, true, false)); - - assertEquals(1, Util.binarySearchCeil(values, 2, false, false)); - assertEquals(1, Util.binarySearchCeil(values, 2, true, false)); - - assertEquals(3, Util.binarySearchCeil(values, 5, false, false)); - assertEquals(2, Util.binarySearchCeil(values, 5, true, false)); - assertEquals(2, Util.binarySearchCeil(values, 5, false, true)); - assertEquals(2, Util.binarySearchCeil(values, 5, true, true)); - - assertEquals(3, Util.binarySearchCeil(values, 6, false, false)); - assertEquals(3, Util.binarySearchCeil(values, 6, true, false)); - assertEquals(2, Util.binarySearchCeil(values, 6, false, true)); - assertEquals(2, Util.binarySearchCeil(values, 6, true, true)); - } - - public void testListBinarySearchCeil() { - List values = new ArrayList<>(); - assertEquals(0, Util.binarySearchCeil(values, 0, false, false)); - assertEquals(-1, Util.binarySearchCeil(values, 0, false, true)); - - values.add(1); - values.add(3); - values.add(5); - assertEquals(0, Util.binarySearchCeil(values, 0, false, false)); - assertEquals(0, Util.binarySearchCeil(values, 0, true, false)); - - assertEquals(1, Util.binarySearchCeil(values, 1, false, false)); - assertEquals(0, Util.binarySearchCeil(values, 1, true, false)); - - assertEquals(1, Util.binarySearchCeil(values, 2, false, false)); - assertEquals(1, Util.binarySearchCeil(values, 2, true, false)); - - assertEquals(3, Util.binarySearchCeil(values, 5, false, false)); - assertEquals(2, Util.binarySearchCeil(values, 5, true, false)); - assertEquals(2, Util.binarySearchCeil(values, 5, false, true)); - assertEquals(2, Util.binarySearchCeil(values, 5, true, true)); - - assertEquals(3, Util.binarySearchCeil(values, 6, false, false)); - assertEquals(3, Util.binarySearchCeil(values, 6, true, false)); - assertEquals(2, Util.binarySearchCeil(values, 6, false, true)); - assertEquals(2, Util.binarySearchCeil(values, 6, true, true)); - } - - public void testParseXsDuration() { - assertEquals(150279L, Util.parseXsDuration("PT150.279S")); - assertEquals(1500L, Util.parseXsDuration("PT1.500S")); - } - - public void testParseXsDateTime() throws Exception { - assertEquals(1403219262000L, Util.parseXsDateTime("2014-06-19T23:07:42")); - assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z")); - assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00,000Z")); - 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() { - assertNull(Util.unescapeFileName("%a")); - assertNull(Util.unescapeFileName("%xyz")); - } - - public void testEscapeUnescapeFileName() { - assertEscapeUnescapeFileName("just+a regular+fileName", "just+a regular+fileName"); - assertEscapeUnescapeFileName("key:value", "key%3avalue"); - assertEscapeUnescapeFileName("<>:\"/\\|?*%", "%3c%3e%3a%22%2f%5c%7c%3f%2a%25"); - - Random random = new Random(0); - for (int i = 0; i < 1000; i++) { - String string = TestUtil.buildTestString(1000, random); - assertEscapeUnescapeFileName(string); - } - } - - private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) { - assertEquals(escapedFileName, Util.escapeFileName(fileName)); - assertEquals(fileName, Util.unescapeFileName(escapedFileName)); - } - - private static void assertEscapeUnescapeFileName(String fileName) { - String escapedFileName = Util.escapeFileName(fileName); - assertEquals(fileName, Util.unescapeFileName(escapedFileName)); - } - -} From daafbb1f1ce0cae9a23c324a073ca38e02ed8a52 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 31 Aug 2017 10:02:43 -0700 Subject: [PATCH 0042/1327] Add missing Robolectric test path to codebase ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167151714 --- .../com/google/android/exoplayer2/CTest.java | 43 ++ .../google/android/exoplayer2/FormatTest.java | 159 ++++++ .../exoplayer2/util/AtomicFileTest.java | 97 ++++ .../exoplayer2/util/ColorParserTest.java | 103 ++++ .../exoplayer2/util/NalUnitUtilTest.java | 217 +++++++ .../exoplayer2/util/ParsableBitArrayTest.java | 198 +++++++ .../util/ParsableByteArrayTest.java | 530 ++++++++++++++++++ .../util/ParsableNalUnitBitArrayTest.java | 128 +++++ .../ReusableBufferedOutputStreamTest.java | 53 ++ .../android/exoplayer2/util/UriUtilTest.java | 109 ++++ .../android/exoplayer2/util/UtilTest.java | 200 +++++++ 11 files changed, 1837 insertions(+) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/CTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/AtomicFileTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java diff --git a/library/core/src/test/java/com/google/android/exoplayer2/CTest.java b/library/core/src/test/java/com/google/android/exoplayer2/CTest.java new file mode 100644 index 0000000000..ff4756f5ed --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/CTest.java @@ -0,0 +1,43 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; + +import android.annotation.SuppressLint; +import android.media.MediaCodec; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link C}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class CTest { + + @SuppressLint("InlinedApi") + @Test + public void testConstants() { + // Sanity check that constant values match those defined by the platform. + assertThat(C.BUFFER_FLAG_KEY_FRAME).isEqualTo(MediaCodec.BUFFER_FLAG_KEY_FRAME); + assertThat(C.BUFFER_FLAG_END_OF_STREAM).isEqualTo(MediaCodec.BUFFER_FLAG_END_OF_STREAM); + assertThat(C.CRYPTO_MODE_AES_CTR).isEqualTo(MediaCodec.CRYPTO_MODE_AES_CTR); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java b/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java new file mode 100644 index 0000000000..8e36edc105 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java @@ -0,0 +1,159 @@ +/* + * 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; + +import static com.google.android.exoplayer2.C.WIDEVINE_UUID; +import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_MP4; +import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_WEBM; +import static com.google.common.truth.Truth.assertThat; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.media.MediaFormat; +import android.os.Parcel; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.ColorInfo; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link Format}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class FormatTest { + + private static final List INIT_DATA; + static { + byte[] initData1 = new byte[] {1, 2, 3}; + byte[] initData2 = new byte[] {4, 5, 6}; + List initData = new ArrayList<>(); + initData.add(initData1); + initData.add(initData2); + INIT_DATA = Collections.unmodifiableList(initData); + } + + @Test + public void testParcelable() { + 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, 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}; + Metadata metadata = new Metadata( + new TextInformationFrame("id1", "description1", "value1"), + new TextInformationFrame("id2", "description2", "value2")); + ColorInfo colorInfo = new ColorInfo(C.COLOR_SPACE_BT709, + C.COLOR_RANGE_LIMITED, C.COLOR_TRANSFER_SDR, new byte[] {1, 2, 3, 4, 5, 6, 7}); + + Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, + 1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, colorInfo, 6, + 44100, C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.NO_VALUE, + Format.OFFSET_SAMPLE_RELATIVE, INIT_DATA, drmInitData, metadata); + + Parcel parcel = Parcel.obtain(); + formatToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + Format formatFromParcel = Format.CREATOR.createFromParcel(parcel); + assertThat(formatFromParcel).isEqualTo(formatToParcel); + + parcel.recycle(); + } + + @Test + public void testConversionToFrameworkMediaFormat() { + if (Util.SDK_INT < 16) { + // Test doesn't apply. + return; + } + + testConversionToFrameworkMediaFormatV16(Format.createVideoSampleFormat(null, "video/xyz", null, + 5000, 102400, 1280, 720, 30, INIT_DATA, null)); + testConversionToFrameworkMediaFormatV16(Format.createVideoSampleFormat(null, "video/xyz", null, + 5000, Format.NO_VALUE, 1280, 720, 30, null, null)); + testConversionToFrameworkMediaFormatV16(Format.createAudioSampleFormat(null, "audio/xyz", null, + 500, 128, 5, 44100, INIT_DATA, null, 0, null)); + testConversionToFrameworkMediaFormatV16(Format.createAudioSampleFormat(null, "audio/xyz", null, + 500, Format.NO_VALUE, 5, 44100, null, null, 0, null)); + testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", 0, + "eng")); + testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", 0, + null)); + } + + @SuppressLint("InlinedApi") + @TargetApi(16) + private static void testConversionToFrameworkMediaFormatV16(Format in) { + MediaFormat out = in.getFrameworkMediaFormatV16(); + assertThat(out.getString(MediaFormat.KEY_MIME)).isEqualTo(in.sampleMimeType); + assertOptionalV16(out, MediaFormat.KEY_LANGUAGE, in.language); + assertOptionalV16(out, MediaFormat.KEY_MAX_INPUT_SIZE, in.maxInputSize); + assertOptionalV16(out, MediaFormat.KEY_WIDTH, in.width); + assertOptionalV16(out, MediaFormat.KEY_HEIGHT, in.height); + assertOptionalV16(out, MediaFormat.KEY_CHANNEL_COUNT, in.channelCount); + assertOptionalV16(out, MediaFormat.KEY_SAMPLE_RATE, in.sampleRate); + assertOptionalV16(out, MediaFormat.KEY_FRAME_RATE, in.frameRate); + + for (int i = 0; i < in.initializationData.size(); i++) { + byte[] originalData = in.initializationData.get(i); + ByteBuffer frameworkBuffer = out.getByteBuffer("csd-" + i); + byte[] frameworkData = Arrays.copyOf(frameworkBuffer.array(), frameworkBuffer.limit()); + assertThat(frameworkData).isEqualTo(originalData); + } + } + + @TargetApi(16) + private static void assertOptionalV16(MediaFormat format, String key, String value) { + if (value == null) { + assertThat(format.containsKey(key)).isEqualTo(false); + } else { + assertThat(format.getString(key)).isEqualTo(value); + } + } + + @TargetApi(16) + private static void assertOptionalV16(MediaFormat format, String key, int value) { + if (value == Format.NO_VALUE) { + assertThat(format.containsKey(key)).isEqualTo(false); + } else { + assertThat(format.getInteger(key)).isEqualTo(value); + } + } + + @TargetApi(16) + private static void assertOptionalV16(MediaFormat format, String key, float value) { + if (value == Format.NO_VALUE) { + assertThat(format.containsKey(key)).isEqualTo(false); + } else { + assertThat(format.getFloat(key)).isEqualTo(value); + } + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/AtomicFileTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/AtomicFileTest.java new file mode 100644 index 0000000000..dcf3d31eb3 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/AtomicFileTest.java @@ -0,0 +1,97 @@ +/* + * 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.util; + +import static com.google.common.truth.Truth.assertThat; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +/** + * Tests {@link AtomicFile}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class AtomicFileTest { + + private File tempFolder; + private File file; + private AtomicFile atomicFile; + + @Before + public void setUp() throws Exception { + tempFolder = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); + file = new File(tempFolder, "atomicFile"); + atomicFile = new AtomicFile(file); + } + + @After + public void tearDown() throws Exception { + Util.recursiveDelete(tempFolder); + } + + @Test + public void testDelete() throws Exception { + assertThat(file.createNewFile()).isTrue(); + atomicFile.delete(); + assertThat(file.exists()).isFalse(); + } + + @Test + public void testWriteRead() throws Exception { + OutputStream output = atomicFile.startWrite(); + output.write(5); + atomicFile.endWrite(output); + output.close(); + + assertRead(); + + output = atomicFile.startWrite(); + output.write(5); + output.write(6); + output.close(); + + assertRead(); + + output = atomicFile.startWrite(); + output.write(6); + + assertRead(); + output.close(); + + output = atomicFile.startWrite(); + + assertRead(); + output.close(); + } + + private void assertRead() throws IOException { + InputStream input = atomicFile.openRead(); + assertThat(input.read()).isEqualTo(5); + assertThat(input.read()).isEqualTo(-1); + input.close(); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java new file mode 100644 index 0000000000..13b126090c --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java @@ -0,0 +1,103 @@ +/* + * 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.util; + +import static android.graphics.Color.BLACK; +import static android.graphics.Color.RED; +import static android.graphics.Color.WHITE; +import static android.graphics.Color.argb; +import static android.graphics.Color.parseColor; +import static com.google.android.exoplayer2.util.ColorParser.parseTtmlColor; +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Color; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for ColorParser. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ColorParserTest { + + // Negative tests. + + @Test(expected = IllegalArgumentException.class) + public void testParseUnknownColor() { + ColorParser.parseTtmlColor("colorOfAnElectron"); + } + + @Test(expected = IllegalArgumentException.class) + public void testParseNull() { + ColorParser.parseTtmlColor(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testParseEmpty() { + ColorParser.parseTtmlColor(""); + } + + @Test(expected = IllegalArgumentException.class) + public void testRgbColorParsingRgbValuesNegative() { + ColorParser.parseTtmlColor("rgb(-4, 55, 209)"); + } + + // Positive tests. + + @Test + public void testHexCodeParsing() { + assertThat(parseTtmlColor("#FFFFFF")).isEqualTo(WHITE); + assertThat(parseTtmlColor("#FFFFFFFF")).isEqualTo(WHITE); + assertThat(parseTtmlColor("#123456")).isEqualTo(parseColor("#FF123456")); + // Hex colors in ColorParser are RGBA, where-as {@link Color#parseColor} takes ARGB. + assertThat(parseTtmlColor("#FFFFFF00")).isEqualTo(parseColor("#00FFFFFF")); + assertThat(parseTtmlColor("#12345678")).isEqualTo(parseColor("#78123456")); + } + + @Test + public void testRgbColorParsing() { + assertThat(parseTtmlColor("rgb(255,255,255)")).isEqualTo(WHITE); + // Spaces are ignored. + assertThat(parseTtmlColor(" rgb ( 255, 255, 255)")).isEqualTo(WHITE); + } + + @Test + public void testRgbColorParsingRgbValuesOutOfBounds() { + int outOfBounds = ColorParser.parseTtmlColor("rgb(999, 999, 999)"); + int color = Color.rgb(999, 999, 999); + // Behave like the framework does. + assertThat(outOfBounds).isEqualTo(color); + } + + @Test + public void testRgbaColorParsing() { + assertThat(parseTtmlColor("rgba(255,255,255,255)")).isEqualTo(WHITE); + assertThat(parseTtmlColor("rgba(255,255,255,255)")) + .isEqualTo(argb(255, 255, 255, 255)); + assertThat(parseTtmlColor("rgba(0, 0, 0, 255)")).isEqualTo(BLACK); + assertThat(parseTtmlColor("rgba(0, 0, 255, 0)")) + .isEqualTo(argb(0, 0, 0, 255)); + assertThat(parseTtmlColor("rgba(255, 0, 0, 255)")).isEqualTo(RED); + assertThat(parseTtmlColor("rgba(255, 0, 255, 0)")) + .isEqualTo(argb(0, 255, 0, 255)); + assertThat(parseTtmlColor("rgba(255, 0, 0, 205)")) + .isEqualTo(argb(205, 255, 0, 0)); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java new file mode 100644 index 0000000000..ee77664cce --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java @@ -0,0 +1,217 @@ +/* + * 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.util; + +import static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; +import static com.google.common.truth.Truth.assertThat; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Tests for {@link NalUnitUtil}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class NalUnitUtilTest { + + private static final int TEST_PARTIAL_NAL_POSITION = 4; + private static final int TEST_NAL_POSITION = 10; + private static final byte[] SPS_TEST_DATA = createByteArray(0x00, 0x00, 0x01, 0x67, 0x4D, 0x40, + 0x16, 0xEC, 0xA0, 0x50, 0x17, 0xFC, 0xB8, 0x08, 0x80, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, + 0x00, 0x0F, 0x47, 0x8B, 0x16, 0xCB); + private static final int SPS_TEST_DATA_OFFSET = 3; + + @Test + public void testFindNalUnit() { + byte[] data = buildTestData(); + + // Should find NAL unit. + int result = NalUnitUtil.findNalUnit(data, 0, data.length, null); + assertThat(result).isEqualTo(TEST_NAL_POSITION); + // Should find NAL unit whose prefix ends one byte before the limit. + result = NalUnitUtil.findNalUnit(data, 0, TEST_NAL_POSITION + 4, null); + assertThat(result).isEqualTo(TEST_NAL_POSITION); + // Shouldn't find NAL unit whose prefix ends at the limit (since the limit is exclusive). + result = NalUnitUtil.findNalUnit(data, 0, TEST_NAL_POSITION + 3, null); + assertThat(result).isEqualTo(TEST_NAL_POSITION + 3); + // Should find NAL unit whose prefix starts at the offset. + result = NalUnitUtil.findNalUnit(data, TEST_NAL_POSITION, data.length, null); + assertThat(result).isEqualTo(TEST_NAL_POSITION); + // Shouldn't find NAL unit whose prefix starts one byte past the offset. + result = NalUnitUtil.findNalUnit(data, TEST_NAL_POSITION + 1, data.length, null); + assertThat(result).isEqualTo(data.length); + } + + @Test + public void testFindNalUnitWithPrefix() { + byte[] data = buildTestData(); + + // First byte of NAL unit in data1, rest in data2. + boolean[] prefixFlags = new boolean[3]; + byte[] data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1); + byte[] data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, data.length); + int result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); + assertThat(result).isEqualTo(data1.length); + result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); + assertThat(result).isEqualTo(-1); + assertPrefixFlagsCleared(prefixFlags); + + // First three bytes of NAL unit in data1, rest in data2. + prefixFlags = new boolean[3]; + data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 3); + data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 3, data.length); + result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); + assertThat(result).isEqualTo(data1.length); + result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); + assertThat(result).isEqualTo(-3); + assertPrefixFlagsCleared(prefixFlags); + + // First byte of NAL unit in data1, second byte in data2, rest in data3. + prefixFlags = new boolean[3]; + data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1); + data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2); + byte[] data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length); + result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); + assertThat(result).isEqualTo(data1.length); + result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); + assertThat(result).isEqualTo(data2.length); + result = NalUnitUtil.findNalUnit(data3, 0, data3.length, prefixFlags); + assertThat(result).isEqualTo(-2); + assertPrefixFlagsCleared(prefixFlags); + + // NAL unit split with one byte in four arrays. + prefixFlags = new boolean[3]; + data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1); + data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2); + data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, TEST_NAL_POSITION + 3); + byte[] data4 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length); + result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); + assertThat(result).isEqualTo(data1.length); + result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); + assertThat(result).isEqualTo(data2.length); + result = NalUnitUtil.findNalUnit(data3, 0, data3.length, prefixFlags); + assertThat(result).isEqualTo(data3.length); + result = NalUnitUtil.findNalUnit(data4, 0, data4.length, prefixFlags); + assertThat(result).isEqualTo(-3); + assertPrefixFlagsCleared(prefixFlags); + + // NAL unit entirely in data2. data1 ends with partial prefix. + prefixFlags = new boolean[3]; + data1 = Arrays.copyOfRange(data, 0, TEST_PARTIAL_NAL_POSITION + 2); + data2 = Arrays.copyOfRange(data, TEST_PARTIAL_NAL_POSITION + 2, data.length); + result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); + assertThat(result).isEqualTo(data1.length); + result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); + assertThat(result).isEqualTo(4); + assertPrefixFlagsCleared(prefixFlags); + } + + @Test + public void testParseSpsNalUnit() { + NalUnitUtil.SpsData data = NalUnitUtil.parseSpsNalUnit(SPS_TEST_DATA, SPS_TEST_DATA_OFFSET, + SPS_TEST_DATA.length); + assertThat(data.width).isEqualTo(640); + assertThat(data.height).isEqualTo(360); + assertThat(data.deltaPicOrderAlwaysZeroFlag).isFalse(); + assertThat(data.frameMbsOnlyFlag).isTrue(); + assertThat(data.frameNumLength).isEqualTo(4); + assertThat(data.picOrderCntLsbLength).isEqualTo(6); + assertThat(data.seqParameterSetId).isEqualTo(0); + assertThat(data.pixelWidthAspectRatio).isEqualTo(1.0f); + assertThat(data.picOrderCountType).isEqualTo(0); + assertThat(data.separateColorPlaneFlag).isFalse(); + } + + @Test + public void testUnescapeDoesNotModifyBuffersWithoutStartCodes() { + assertUnescapeDoesNotModify(""); + assertUnescapeDoesNotModify("0000"); + assertUnescapeDoesNotModify("172BF38A3C"); + assertUnescapeDoesNotModify("000004"); + } + + @Test + public void testUnescapeModifiesBuffersWithStartCodes() { + assertUnescapeMatchesExpected("00000301", "000001"); + assertUnescapeMatchesExpected("0000030200000300", "000002000000"); + } + + @Test + public void testDiscardToSps() { + assertDiscardToSpsMatchesExpected("", ""); + assertDiscardToSpsMatchesExpected("00", ""); + assertDiscardToSpsMatchesExpected("FFFF000001", ""); + assertDiscardToSpsMatchesExpected("00000001", ""); + assertDiscardToSpsMatchesExpected("00000001FF67", ""); + assertDiscardToSpsMatchesExpected("00000001000167", ""); + assertDiscardToSpsMatchesExpected("0000000167", "0000000167"); + assertDiscardToSpsMatchesExpected("0000000167FF", "0000000167FF"); + assertDiscardToSpsMatchesExpected("0000000167FF", "0000000167FF"); + assertDiscardToSpsMatchesExpected("0000000167FF000000016700", "0000000167FF000000016700"); + assertDiscardToSpsMatchesExpected("000000000167FF", "0000000167FF"); + assertDiscardToSpsMatchesExpected("0001670000000167FF", "0000000167FF"); + assertDiscardToSpsMatchesExpected("FF00000001660000000167FF", "0000000167FF"); + } + + private static byte[] buildTestData() { + byte[] data = new byte[20]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) 0xFF; + } + // Insert an incomplete NAL unit start code. + data[TEST_PARTIAL_NAL_POSITION] = 0; + data[TEST_PARTIAL_NAL_POSITION + 1] = 0; + // Insert a complete NAL unit start code. + data[TEST_NAL_POSITION] = 0; + data[TEST_NAL_POSITION + 1] = 0; + data[TEST_NAL_POSITION + 2] = 1; + data[TEST_NAL_POSITION + 3] = 5; + return data; + } + + private static void assertPrefixFlagsCleared(boolean[] flags) { + assertThat(flags[0] || flags[1] || flags[2]).isEqualTo(false); + } + + private static void assertUnescapeDoesNotModify(String input) { + assertUnescapeMatchesExpected(input, input); + } + + private static void assertUnescapeMatchesExpected(String input, String expectedOutput) { + byte[] bitstream = Util.getBytesFromHexString(input); + byte[] expectedOutputBitstream = Util.getBytesFromHexString(expectedOutput); + int count = NalUnitUtil.unescapeStream(bitstream, bitstream.length); + assertThat(count).isEqualTo(expectedOutputBitstream.length); + byte[] outputBitstream = new byte[count]; + System.arraycopy(bitstream, 0, outputBitstream, 0, count); + assertThat(outputBitstream).isEqualTo(expectedOutputBitstream); + } + + private static void assertDiscardToSpsMatchesExpected(String input, String expectedOutput) { + byte[] bitstream = Util.getBytesFromHexString(input); + byte[] expectedOutputBitstream = Util.getBytesFromHexString(expectedOutput); + ByteBuffer buffer = ByteBuffer.wrap(bitstream); + buffer.position(buffer.limit()); + NalUnitUtil.discardToSps(buffer); + assertThat(Arrays.copyOf(buffer.array(), buffer.position())).isEqualTo(expectedOutputBitstream); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java new file mode 100644 index 0000000000..0d864f407f --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java @@ -0,0 +1,198 @@ +/* + * 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.util; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Tests for {@link ParsableBitArray}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ParsableBitArrayTest { + + private static final byte[] TEST_DATA = new byte[] {0x3C, (byte) 0xD2, (byte) 0x5F, (byte) 0x01, + (byte) 0xFF, (byte) 0x14, (byte) 0x60, (byte) 0x99}; + + private ParsableBitArray testArray; + + @Before + public void setUp() { + testArray = new ParsableBitArray(TEST_DATA); + } + + @Test + public void testReadAllBytes() { + byte[] bytesRead = new byte[TEST_DATA.length]; + testArray.readBytes(bytesRead, 0, TEST_DATA.length); + assertThat(bytesRead).isEqualTo(TEST_DATA); + assertThat(testArray.getPosition()).isEqualTo(TEST_DATA.length * 8); + assertThat(testArray.getBytePosition()).isEqualTo(TEST_DATA.length); + } + + @Test + public void testReadBit() { + assertReadBitsToEnd(0); + } + + @Test + public void testReadBits() { + assertThat(testArray.readBits(5)).isEqualTo(getTestDataBits(0, 5)); + assertThat(testArray.readBits(0)).isEqualTo(getTestDataBits(5, 0)); + assertThat(testArray.readBits(3)).isEqualTo(getTestDataBits(5, 3)); + assertThat(testArray.readBits(16)).isEqualTo(getTestDataBits(8, 16)); + assertThat(testArray.readBits(3)).isEqualTo(getTestDataBits(24, 3)); + assertThat(testArray.readBits(18)).isEqualTo(getTestDataBits(27, 18)); + assertThat(testArray.readBits(5)).isEqualTo(getTestDataBits(45, 5)); + assertThat(testArray.readBits(14)).isEqualTo(getTestDataBits(50, 14)); + } + + @Test + public void testReadBitsToByteArray() { + byte[] result = new byte[TEST_DATA.length]; + // Test read within byte boundaries. + testArray.readBits(result, 0, 6); + assertThat(result[0]).isEqualTo((byte) (TEST_DATA[0] & 0xFC)); + // Test read across byte boundaries. + testArray.readBits(result, 0, 8); + assertThat(result[0]).isEqualTo( + (byte) (((TEST_DATA[0] & 0x03) << 6) | ((TEST_DATA[1] & 0xFC) >> 2))); + // Test reading across multiple bytes. + testArray.readBits(result, 1, 50); + for (int i = 1; i < 7; i++) { + assertThat(result[i]) + .isEqualTo((byte) (((TEST_DATA[i] & 0x03) << 6) | ((TEST_DATA[i + 1] & 0xFC) >> 2))); + } + assertThat(result[7]).isEqualTo((byte) ((TEST_DATA[7] & 0x03) << 6)); + assertThat(testArray.bitsLeft()).isEqualTo(0); + // Test read last buffer byte across input data bytes. + testArray.setPosition(31); + result[3] = 0; + testArray.readBits(result, 3, 3); + assertThat(result[3]).isEqualTo((byte) 0xE0); + // Test read bits in the middle of a input data byte. + result[0] = 0; + assertThat(testArray.getPosition()).isEqualTo(34); + testArray.readBits(result, 0, 3); + assertThat(result[0]).isEqualTo((byte) 0xE0); + // Test read 0 bits. + testArray.setPosition(32); + result[1] = 0; + testArray.readBits(result, 1, 0); + assertThat(result[1]).isEqualTo((byte) 0); + // Test reading a number of bits divisible by 8. + testArray.setPosition(0); + testArray.readBits(result, 0, 16); + assertThat(result[0]).isEqualTo(TEST_DATA[0]); + assertThat(result[1]).isEqualTo(TEST_DATA[1]); + // Test least significant bits are unmodified. + result[1] = (byte) 0xFF; + testArray.readBits(result, 0, 9); + assertThat(result[0]).isEqualTo((byte) 0x5F); + assertThat(result[1]).isEqualTo((byte) 0x7F); + } + + @Test + public void testRead32BitsByteAligned() { + assertThat(testArray.readBits(32)).isEqualTo(getTestDataBits(0, 32)); + assertThat(testArray.readBits(32)).isEqualTo(getTestDataBits(32, 32)); + } + + @Test + public void testRead32BitsNonByteAligned() { + assertThat(testArray.readBits(5)).isEqualTo(getTestDataBits(0, 5)); + assertThat(testArray.readBits(32)).isEqualTo(getTestDataBits(5, 32)); + } + + @Test + public void testSkipBytes() { + testArray.skipBytes(2); + assertReadBitsToEnd(16); + } + + @Test + public void testSkipBitsByteAligned() { + testArray.skipBits(16); + assertReadBitsToEnd(16); + } + + @Test + public void testSkipBitsNonByteAligned() { + testArray.skipBits(5); + assertReadBitsToEnd(5); + } + + @Test + public void testSetPositionByteAligned() { + testArray.setPosition(16); + assertReadBitsToEnd(16); + } + + @Test + public void testSetPositionNonByteAligned() { + testArray.setPosition(5); + assertReadBitsToEnd(5); + } + + @Test + public void testByteAlignFromNonByteAligned() { + testArray.setPosition(11); + testArray.byteAlign(); + assertThat(testArray.getBytePosition()).isEqualTo(2); + assertThat(testArray.getPosition()).isEqualTo(16); + assertReadBitsToEnd(16); + } + + @Test + public void testByteAlignFromByteAligned() { + testArray.setPosition(16); + testArray.byteAlign(); // Should be a no-op. + assertThat(testArray.getBytePosition()).isEqualTo(2); + assertThat(testArray.getPosition()).isEqualTo(16); + assertReadBitsToEnd(16); + } + + private void assertReadBitsToEnd(int expectedStartPosition) { + int position = testArray.getPosition(); + assertThat(position).isEqualTo(expectedStartPosition); + for (int i = position; i < TEST_DATA.length * 8; i++) { + assertThat(testArray.readBit()).isEqualTo(getTestDataBit(i)); + assertThat(testArray.getPosition()).isEqualTo(i + 1); + } + } + + private static int getTestDataBits(int bitPosition, int length) { + int result = 0; + for (int i = 0; i < length; i++) { + result = result << 1; + if (getTestDataBit(bitPosition++)) { + result |= 0x1; + } + } + return result; + } + + private static boolean getTestDataBit(int bitPosition) { + return (TEST_DATA[bitPosition / 8] & (0x80 >>> (bitPosition % 8))) != 0; + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java new file mode 100644 index 0000000000..504a58b4a8 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java @@ -0,0 +1,530 @@ +/* + * 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.util; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.Charset.forName; +import static junit.framework.TestCase.fail; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Tests for {@link ParsableByteArray}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ParsableByteArrayTest { + + private static final byte[] TEST_DATA = + new byte[] {0x0F, (byte) 0xFF, (byte) 0x42, (byte) 0x0F, 0x00, 0x00, 0x00, 0x00}; + + private static ParsableByteArray getTestDataArray() { + ParsableByteArray testArray = new ParsableByteArray(TEST_DATA.length); + System.arraycopy(TEST_DATA, 0, testArray.data, 0, TEST_DATA.length); + return testArray; + } + + @Test + public void testReadShort() { + testReadShort((short) -1); + testReadShort((short) 0); + testReadShort((short) 1); + testReadShort(Short.MIN_VALUE); + testReadShort(Short.MAX_VALUE); + } + + private static void testReadShort(short testValue) { + ParsableByteArray testArray = new ParsableByteArray( + ByteBuffer.allocate(4).putShort(testValue).array()); + int readValue = testArray.readShort(); + + // Assert that the value we read was the value we wrote. + assertThat(readValue).isEqualTo(testValue); + // And that the position advanced as expected. + assertThat(testArray.getPosition()).isEqualTo(2); + + // And that skipping back and reading gives the same results. + testArray.skipBytes(-2); + readValue = testArray.readShort(); + assertThat(readValue).isEqualTo(testValue); + assertThat(testArray.getPosition()).isEqualTo(2); + } + + @Test + public void testReadInt() { + testReadInt(0); + testReadInt(1); + testReadInt(-1); + testReadInt(Integer.MIN_VALUE); + testReadInt(Integer.MAX_VALUE); + } + + private static void testReadInt(int testValue) { + ParsableByteArray testArray = new ParsableByteArray( + ByteBuffer.allocate(4).putInt(testValue).array()); + int readValue = testArray.readInt(); + + // Assert that the value we read was the value we wrote. + assertThat(readValue).isEqualTo(testValue); + // And that the position advanced as expected. + assertThat(testArray.getPosition()).isEqualTo(4); + + // And that skipping back and reading gives the same results. + testArray.skipBytes(-4); + readValue = testArray.readInt(); + assertThat(readValue).isEqualTo(testValue); + assertThat(testArray.getPosition()).isEqualTo(4); + } + + @Test + public void testReadUnsignedInt() { + testReadUnsignedInt(0); + testReadUnsignedInt(1); + testReadUnsignedInt(Integer.MAX_VALUE); + testReadUnsignedInt(Integer.MAX_VALUE + 1L); + testReadUnsignedInt(0xFFFFFFFFL); + } + + private static void testReadUnsignedInt(long testValue) { + ParsableByteArray testArray = new ParsableByteArray( + Arrays.copyOfRange(ByteBuffer.allocate(8).putLong(testValue).array(), 4, 8)); + long readValue = testArray.readUnsignedInt(); + + // Assert that the value we read was the value we wrote. + assertThat(readValue).isEqualTo(testValue); + // And that the position advanced as expected. + assertThat(testArray.getPosition()).isEqualTo(4); + + // And that skipping back and reading gives the same results. + testArray.skipBytes(-4); + readValue = testArray.readUnsignedInt(); + assertThat(readValue).isEqualTo(testValue); + assertThat(testArray.getPosition()).isEqualTo(4); + } + + @Test + public void testReadUnsignedIntToInt() { + testReadUnsignedIntToInt(0); + testReadUnsignedIntToInt(1); + testReadUnsignedIntToInt(Integer.MAX_VALUE); + try { + testReadUnsignedIntToInt(-1); + fail(); + } catch (IllegalStateException e) { + // Expected. + } + try { + testReadUnsignedIntToInt(Integer.MIN_VALUE); + fail(); + } catch (IllegalStateException e) { + // Expected. + } + } + + private static void testReadUnsignedIntToInt(int testValue) { + ParsableByteArray testArray = new ParsableByteArray( + ByteBuffer.allocate(4).putInt(testValue).array()); + int readValue = testArray.readUnsignedIntToInt(); + + // Assert that the value we read was the value we wrote. + assertThat(readValue).isEqualTo(testValue); + // And that the position advanced as expected. + assertThat(testArray.getPosition()).isEqualTo(4); + + // And that skipping back and reading gives the same results. + testArray.skipBytes(-4); + readValue = testArray.readUnsignedIntToInt(); + assertThat(readValue).isEqualTo(testValue); + assertThat(testArray.getPosition()).isEqualTo(4); + } + + @Test + public void testReadUnsignedLongToLong() { + testReadUnsignedLongToLong(0); + testReadUnsignedLongToLong(1); + testReadUnsignedLongToLong(Long.MAX_VALUE); + try { + testReadUnsignedLongToLong(-1); + fail(); + } catch (IllegalStateException e) { + // Expected. + } + try { + testReadUnsignedLongToLong(Long.MIN_VALUE); + fail(); + } catch (IllegalStateException e) { + // Expected. + } + } + + private static void testReadUnsignedLongToLong(long testValue) { + ParsableByteArray testArray = new ParsableByteArray( + ByteBuffer.allocate(8).putLong(testValue).array()); + long readValue = testArray.readUnsignedLongToLong(); + + // Assert that the value we read was the value we wrote. + assertThat(readValue).isEqualTo(testValue); + // And that the position advanced as expected. + assertThat(testArray.getPosition()).isEqualTo(8); + + // And that skipping back and reading gives the same results. + testArray.skipBytes(-8); + readValue = testArray.readUnsignedLongToLong(); + assertThat(readValue).isEqualTo(testValue); + assertThat(testArray.getPosition()).isEqualTo(8); + } + + @Test + public void testReadLong() { + testReadLong(0); + testReadLong(1); + testReadLong(-1); + testReadLong(Long.MIN_VALUE); + testReadLong(Long.MAX_VALUE); + } + + private static void testReadLong(long testValue) { + ParsableByteArray testArray = new ParsableByteArray( + ByteBuffer.allocate(8).putLong(testValue).array()); + long readValue = testArray.readLong(); + + // Assert that the value we read was the value we wrote. + assertThat(readValue).isEqualTo(testValue); + // And that the position advanced as expected. + assertThat(testArray.getPosition()).isEqualTo(8); + + // And that skipping back and reading gives the same results. + testArray.skipBytes(-8); + readValue = testArray.readLong(); + assertThat(readValue).isEqualTo(testValue); + assertThat(testArray.getPosition()).isEqualTo(8); + } + + @Test + public void testReadingMovesPosition() { + ParsableByteArray parsableByteArray = getTestDataArray(); + + // Given an array at the start + assertThat(parsableByteArray.getPosition()).isEqualTo(0); + // When reading an integer, the position advances + parsableByteArray.readUnsignedInt(); + assertThat(parsableByteArray.getPosition()).isEqualTo(4); + } + + @Test + public void testOutOfBoundsThrows() { + ParsableByteArray parsableByteArray = getTestDataArray(); + + // Given an array at the end + parsableByteArray.readUnsignedLongToLong(); + assertThat(parsableByteArray.getPosition()).isEqualTo(TEST_DATA.length); + // Then reading more data throws. + try { + parsableByteArray.readUnsignedInt(); + fail(); + } catch (Exception e) { + // Expected. + } + } + + @Test + public void testModificationsAffectParsableArray() { + ParsableByteArray parsableByteArray = getTestDataArray(); + + // When modifying the wrapped byte array + byte[] data = parsableByteArray.data; + long readValue = parsableByteArray.readUnsignedInt(); + data[0] = (byte) (TEST_DATA[0] + 1); + parsableByteArray.setPosition(0); + // Then the parsed value changes. + assertThat(parsableByteArray.readUnsignedInt()).isNotEqualTo(readValue); + } + + @Test + public void testReadingUnsignedLongWithMsbSetThrows() { + ParsableByteArray parsableByteArray = getTestDataArray(); + + // Given an array with the most-significant bit set on the top byte + byte[] data = parsableByteArray.data; + data[0] = (byte) 0x80; + // Then reading an unsigned long throws. + try { + parsableByteArray.readUnsignedLongToLong(); + fail(); + } catch (Exception e) { + // Expected. + } + } + + @Test + public void testReadUnsignedFixedPoint1616() { + ParsableByteArray parsableByteArray = getTestDataArray(); + + // When reading the integer part of a 16.16 fixed point value + int value = parsableByteArray.readUnsignedFixedPoint1616(); + // Then the read value is equal to the array elements interpreted as a short. + assertThat(value).isEqualTo((0xFF & TEST_DATA[0]) << 8 | (TEST_DATA[1] & 0xFF)); + assertThat(parsableByteArray.getPosition()).isEqualTo(4); + } + + @Test + public void testReadingBytesReturnsCopy() { + ParsableByteArray parsableByteArray = getTestDataArray(); + + // When reading all the bytes back + int length = parsableByteArray.limit(); + assertThat(length).isEqualTo(TEST_DATA.length); + byte[] copy = new byte[length]; + parsableByteArray.readBytes(copy, 0, length); + // Then the array elements are the same. + assertThat(copy).isEqualTo(parsableByteArray.data); + } + + @Test + public void testReadLittleEndianLong() { + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, (byte) 0xFF + }); + assertThat(byteArray.readLittleEndianLong()).isEqualTo(0xFF00000000000001L); + assertThat(byteArray.getPosition()).isEqualTo(8); + } + + @Test + public void testReadLittleEndianUnsignedInt() { + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { + 0x10, 0x00, 0x00, (byte) 0xFF + }); + assertThat(byteArray.readLittleEndianUnsignedInt()).isEqualTo(0xFF000010L); + assertThat(byteArray.getPosition()).isEqualTo(4); + } + + @Test + public void testReadLittleEndianInt() { + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { + 0x01, 0x00, 0x00, (byte) 0xFF + }); + assertThat(byteArray.readLittleEndianInt()).isEqualTo(0xFF000001); + assertThat(byteArray.getPosition()).isEqualTo(4); + } + + @Test + public void testReadLittleEndianUnsignedInt24() { + byte[] data = { 0x01, 0x02, (byte) 0xFF }; + ParsableByteArray byteArray = new ParsableByteArray(data); + assertThat(byteArray.readLittleEndianUnsignedInt24()).isEqualTo(0xFF0201); + assertThat(byteArray.getPosition()).isEqualTo(3); + } + + @Test + public void testReadLittleEndianUnsignedShort() { + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { + 0x01, (byte) 0xFF, 0x02, (byte) 0xFF + }); + assertThat(byteArray.readLittleEndianUnsignedShort()).isEqualTo(0xFF01); + assertThat(byteArray.getPosition()).isEqualTo(2); + assertThat(byteArray.readLittleEndianUnsignedShort()).isEqualTo(0xFF02); + assertThat(byteArray.getPosition()).isEqualTo(4); + } + + @Test + public void testReadLittleEndianShort() { + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { + 0x01, (byte) 0xFF, 0x02, (byte) 0xFF + }); + assertThat(byteArray.readLittleEndianShort()).isEqualTo((short) 0xFF01); + assertThat(byteArray.getPosition()).isEqualTo(2); + assertThat(byteArray.readLittleEndianShort()).isEqualTo((short) 0xFF02); + assertThat(byteArray.getPosition()).isEqualTo(4); + } + + @Test + public void testReadString() { + byte[] data = { + (byte) 0xC3, (byte) 0xA4, (byte) 0x20, + (byte) 0xC3, (byte) 0xB6, (byte) 0x20, + (byte) 0xC2, (byte) 0xAE, (byte) 0x20, + (byte) 0xCF, (byte) 0x80, (byte) 0x20, + (byte) 0xE2, (byte) 0x88, (byte) 0x9A, (byte) 0x20, + (byte) 0xC2, (byte) 0xB1, (byte) 0x20, + (byte) 0xE8, (byte) 0xB0, (byte) 0xA2, (byte) 0x20, + }; + ParsableByteArray byteArray = new ParsableByteArray(data); + assertThat(byteArray.readString(data.length)).isEqualTo("ä ö ® π √ ± 谢 "); + assertThat(byteArray.getPosition()).isEqualTo(data.length); + } + + @Test + public void testReadAsciiString() { + byte[] data = new byte[] {'t', 'e', 's', 't'}; + ParsableByteArray testArray = new ParsableByteArray(data); + assertThat(testArray.readString(data.length, forName("US-ASCII"))).isEqualTo("test"); + assertThat(testArray.getPosition()).isEqualTo(data.length); + } + + @Test + public void testReadStringOutOfBoundsDoesNotMovePosition() { + byte[] data = { + (byte) 0xC3, (byte) 0xA4, (byte) 0x20 + }; + ParsableByteArray byteArray = new ParsableByteArray(data); + try { + byteArray.readString(data.length + 1); + fail(); + } catch (StringIndexOutOfBoundsException e) { + assertThat(byteArray.getPosition()).isEqualTo(0); + } + } + + @Test + public void testReadEmptyString() { + byte[] bytes = new byte[0]; + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readLine()).isNull(); + } + + @Test + public void testReadNullTerminatedStringWithLengths() { + byte[] bytes = new byte[] { + 'f', 'o', 'o', 0, 'b', 'a', 'r', 0 + }; + // Test with lengths that match NUL byte positions. + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readNullTerminatedString(4)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(4); + assertThat(parser.readNullTerminatedString(4)).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(8); + assertThat(parser.readNullTerminatedString()).isNull(); + // Test with lengths that do not match NUL byte positions. + parser = new ParsableByteArray(bytes); + assertThat(parser.readNullTerminatedString(2)).isEqualTo("fo"); + assertThat(parser.getPosition()).isEqualTo(2); + assertThat(parser.readNullTerminatedString(2)).isEqualTo("o"); + assertThat(parser.getPosition()).isEqualTo(4); + assertThat(parser.readNullTerminatedString(3)).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(7); + assertThat(parser.readNullTerminatedString(1)).isEqualTo(""); + assertThat(parser.getPosition()).isEqualTo(8); + assertThat(parser.readNullTerminatedString()).isNull(); + // Test with limit at NUL + parser = new ParsableByteArray(bytes, 4); + assertThat(parser.readNullTerminatedString(4)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(4); + assertThat(parser.readNullTerminatedString()).isNull(); + // Test with limit before NUL + parser = new ParsableByteArray(bytes, 3); + assertThat(parser.readNullTerminatedString(3)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(3); + assertThat(parser.readNullTerminatedString()).isNull(); + } + + @Test + public void testReadNullTerminatedString() { + byte[] bytes = new byte[] { + 'f', 'o', 'o', 0, 'b', 'a', 'r', 0 + }; + // Test normal case. + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readNullTerminatedString()).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(4); + assertThat(parser.readNullTerminatedString()).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(8); + assertThat(parser.readNullTerminatedString()).isNull(); + // Test with limit at NUL. + parser = new ParsableByteArray(bytes, 4); + assertThat(parser.readNullTerminatedString()).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(4); + assertThat(parser.readNullTerminatedString()).isNull(); + // Test with limit before NUL. + parser = new ParsableByteArray(bytes, 3); + assertThat(parser.readNullTerminatedString()).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(3); + assertThat(parser.readNullTerminatedString()).isNull(); + } + + @Test + public void testReadNullTerminatedStringWithoutEndingNull() { + byte[] bytes = new byte[] { + 'f', 'o', 'o', 0, 'b', 'a', 'r' + }; + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readNullTerminatedString()).isEqualTo("foo"); + assertThat(parser.readNullTerminatedString()).isEqualTo("bar"); + assertThat(parser.readNullTerminatedString()).isNull(); + } + + @Test + public void testReadSingleLineWithoutEndingTrail() { + byte[] bytes = new byte[] { + 'f', 'o', 'o' + }; + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readLine()).isEqualTo("foo"); + assertThat(parser.readLine()).isNull(); + } + + @Test + public void testReadSingleLineWithEndingLf() { + byte[] bytes = new byte[] { + 'f', 'o', 'o', '\n' + }; + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readLine()).isEqualTo("foo"); + assertThat(parser.readLine()).isNull(); + } + + @Test + public void testReadTwoLinesWithCrFollowedByLf() { + byte[] bytes = new byte[] { + 'f', 'o', 'o', '\r', '\n', 'b', 'a', 'r' + }; + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readLine()).isEqualTo("foo"); + assertThat(parser.readLine()).isEqualTo("bar"); + assertThat(parser.readLine()).isNull(); + } + + @Test + public void testReadThreeLinesWithEmptyLine() { + byte[] bytes = new byte[] { + 'f', 'o', 'o', '\r', '\n', '\r', 'b', 'a', 'r' + }; + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readLine()).isEqualTo("foo"); + assertThat(parser.readLine()).isEqualTo(""); + assertThat(parser.readLine()).isEqualTo("bar"); + assertThat(parser.readLine()).isNull(); + } + + @Test + public void testReadFourLinesWithLfFollowedByCr() { + byte[] bytes = new byte[] { + 'f', 'o', 'o', '\n', '\r', '\r', 'b', 'a', 'r', '\r', '\n' + }; + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readLine()).isEqualTo("foo"); + assertThat(parser.readLine()).isEqualTo(""); + assertThat(parser.readLine()).isEqualTo(""); + assertThat(parser.readLine()).isEqualTo("bar"); + assertThat(parser.readLine()).isNull(); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java new file mode 100644 index 0000000000..a3f38abcdb --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java @@ -0,0 +1,128 @@ +/* + * 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.util; + +import static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Tests for {@link ParsableNalUnitBitArray}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ParsableNalUnitBitArrayTest { + + private static final byte[] NO_ESCAPING_TEST_DATA = createByteArray(0, 3, 0, 1, 3, 0, 0); + private static final byte[] ALL_ESCAPING_TEST_DATA = createByteArray(0, 0, 3, 0, 0, 3, 0, 0, 3); + private static final byte[] MIX_TEST_DATA = createByteArray(255, 0, 0, 3, 255, 0, 0, 127); + + @Test + public void testReadNoEscaping() { + ParsableNalUnitBitArray array = + new ParsableNalUnitBitArray(NO_ESCAPING_TEST_DATA, 0, NO_ESCAPING_TEST_DATA.length); + assertThat(array.readBits(24)).isEqualTo(0x000300); + assertThat(array.readBits(7)).isEqualTo(0); + assertThat(array.readBit()).isTrue(); + assertThat(array.readBits(24)).isEqualTo(0x030000); + assertThat(array.canReadBits(1)).isFalse(); + assertThat(array.canReadBits(8)).isFalse(); + } + + @Test + public void testReadNoEscapingTruncated() { + ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(NO_ESCAPING_TEST_DATA, 0, 4); + assertThat(array.canReadBits(32)).isTrue(); + array.skipBits(32); + assertThat(array.canReadBits(1)).isFalse(); + try { + array.readBit(); + fail(); + } catch (Exception e) { + // Expected. + } + } + + @Test + public void testReadAllEscaping() { + ParsableNalUnitBitArray array = + new ParsableNalUnitBitArray(ALL_ESCAPING_TEST_DATA, 0, ALL_ESCAPING_TEST_DATA.length); + assertThat(array.canReadBits(48)).isTrue(); + assertThat(array.canReadBits(49)).isFalse(); + assertThat(array.readBits(15)).isEqualTo(0); + assertThat(array.readBit()).isFalse(); + assertThat(array.readBits(17)).isEqualTo(0); + assertThat(array.readBits(15)).isEqualTo(0); + } + + @Test + public void testReadMix() { + ParsableNalUnitBitArray array = + new ParsableNalUnitBitArray(MIX_TEST_DATA, 0, MIX_TEST_DATA.length); + assertThat(array.canReadBits(56)).isTrue(); + assertThat(array.canReadBits(57)).isFalse(); + assertThat(array.readBits(7)).isEqualTo(127); + assertThat(array.readBits(2)).isEqualTo(2); + assertThat(array.readBits(17)).isEqualTo(3); + assertThat(array.readBits(7)).isEqualTo(126); + assertThat(array.readBits(23)).isEqualTo(127); + assertThat(array.canReadBits(1)).isFalse(); + } + + @Test + public void testReadExpGolomb() { + ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(createByteArray(0x9E), 0, 1); + assertThat(array.canReadExpGolombCodedNum()).isTrue(); + assertThat(array.readUnsignedExpGolombCodedInt()).isEqualTo(0); + assertThat(array.readUnsignedExpGolombCodedInt()).isEqualTo(6); + assertThat(array.readUnsignedExpGolombCodedInt()).isEqualTo(0); + assertThat(array.canReadExpGolombCodedNum()).isFalse(); + try { + array.readUnsignedExpGolombCodedInt(); + fail(); + } catch (Exception e) { + // Expected. + } + } + + @Test + public void testReadExpGolombWithEscaping() { + ParsableNalUnitBitArray array = + new ParsableNalUnitBitArray(createByteArray(0, 0, 3, 128, 0), 0, 5); + assertThat(array.canReadExpGolombCodedNum()).isFalse(); + array.skipBit(); + assertThat(array.canReadExpGolombCodedNum()).isTrue(); + assertThat(array.readUnsignedExpGolombCodedInt()).isEqualTo(32767); + assertThat(array.canReadBits(1)).isFalse(); + } + + @Test + public void testReset() { + ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(createByteArray(0, 0), 0, 2); + assertThat(array.canReadExpGolombCodedNum()).isFalse(); + assertThat(array.canReadBits(16)).isTrue(); + assertThat(array.canReadBits(17)).isFalse(); + array.reset(createByteArray(0, 0, 3, 0), 0, 4); + assertThat(array.canReadBits(24)).isTrue(); + assertThat(array.canReadBits(25)).isFalse(); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java new file mode 100644 index 0000000000..8e384bbb10 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java @@ -0,0 +1,53 @@ +/* + * 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.util; + +import static com.google.common.truth.Truth.assertThat; + +import java.io.ByteArrayOutputStream; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Tests {@link ReusableBufferedOutputStream}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ReusableBufferedOutputStreamTest { + + private static final byte[] TEST_DATA_1 = "test data 1".getBytes(); + private static final byte[] TEST_DATA_2 = "2 test data".getBytes(); + + @Test + public void testReset() throws Exception { + ByteArrayOutputStream byteArrayOutputStream1 = new ByteArrayOutputStream(1000); + ReusableBufferedOutputStream outputStream = new ReusableBufferedOutputStream( + byteArrayOutputStream1, 1000); + outputStream.write(TEST_DATA_1); + outputStream.close(); + + ByteArrayOutputStream byteArrayOutputStream2 = new ByteArrayOutputStream(1000); + outputStream.reset(byteArrayOutputStream2); + outputStream.write(TEST_DATA_2); + outputStream.close(); + + assertThat(byteArrayOutputStream1.toByteArray()).isEqualTo(TEST_DATA_1); + assertThat(byteArrayOutputStream2.toByteArray()).isEqualTo(TEST_DATA_2); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java new file mode 100644 index 0000000000..52e7a722fb --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java @@ -0,0 +1,109 @@ +/* + * 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.util; + +import static com.google.android.exoplayer2.util.UriUtil.resolve; +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit tests for {@link UriUtil}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class UriUtilTest { + + /** + * Tests normal usage of {@link UriUtil#resolve(String, String)}. + *

    + * The test cases are taken from RFC-3986 5.4.1. + */ + @Test + public void testResolveNormal() { + String base = "http://a/b/c/d;p?q"; + + assertThat(resolve(base, "g:h")).isEqualTo("g:h"); + assertThat(resolve(base, "g")).isEqualTo("http://a/b/c/g"); + assertThat(resolve(base, "g/")).isEqualTo("http://a/b/c/g/"); + assertThat(resolve(base, "/g")).isEqualTo("http://a/g"); + assertThat(resolve(base, "//g")).isEqualTo("http://g"); + assertThat(resolve(base, "?y")).isEqualTo("http://a/b/c/d;p?y"); + assertThat(resolve(base, "g?y")).isEqualTo("http://a/b/c/g?y"); + assertThat(resolve(base, "#s")).isEqualTo("http://a/b/c/d;p?q#s"); + assertThat(resolve(base, "g#s")).isEqualTo("http://a/b/c/g#s"); + assertThat(resolve(base, "g?y#s")).isEqualTo("http://a/b/c/g?y#s"); + assertThat(resolve(base, ";x")).isEqualTo("http://a/b/c/;x"); + assertThat(resolve(base, "g;x")).isEqualTo("http://a/b/c/g;x"); + assertThat(resolve(base, "g;x?y#s")).isEqualTo("http://a/b/c/g;x?y#s"); + assertThat(resolve(base, "")).isEqualTo("http://a/b/c/d;p?q"); + assertThat(resolve(base, ".")).isEqualTo("http://a/b/c/"); + assertThat(resolve(base, "./")).isEqualTo("http://a/b/c/"); + assertThat(resolve(base, "..")).isEqualTo("http://a/b/"); + assertThat(resolve(base, "../")).isEqualTo("http://a/b/"); + assertThat(resolve(base, "../g")).isEqualTo("http://a/b/g"); + assertThat(resolve(base, "../..")).isEqualTo("http://a/"); + assertThat(resolve(base, "../../")).isEqualTo("http://a/"); + assertThat(resolve(base, "../../g")).isEqualTo("http://a/g"); + } + + /** + * Tests abnormal usage of {@link UriUtil#resolve(String, String)}. + *

    + * The test cases are taken from RFC-3986 5.4.2. + */ + @Test + public void testResolveAbnormal() { + String base = "http://a/b/c/d;p?q"; + + assertThat(resolve(base, "../../../g")).isEqualTo("http://a/g"); + assertThat(resolve(base, "../../../../g")).isEqualTo("http://a/g"); + + assertThat(resolve(base, "/./g")).isEqualTo("http://a/g"); + assertThat(resolve(base, "/../g")).isEqualTo("http://a/g"); + assertThat(resolve(base, "g.")).isEqualTo("http://a/b/c/g."); + assertThat(resolve(base, ".g")).isEqualTo("http://a/b/c/.g"); + assertThat(resolve(base, "g..")).isEqualTo("http://a/b/c/g.."); + assertThat(resolve(base, "..g")).isEqualTo("http://a/b/c/..g"); + + assertThat(resolve(base, "./../g")).isEqualTo("http://a/b/g"); + assertThat(resolve(base, "./g/.")).isEqualTo("http://a/b/c/g/"); + assertThat(resolve(base, "g/./h")).isEqualTo("http://a/b/c/g/h"); + assertThat(resolve(base, "g/../h")).isEqualTo("http://a/b/c/h"); + assertThat(resolve(base, "g;x=1/./y")).isEqualTo("http://a/b/c/g;x=1/y"); + assertThat(resolve(base, "g;x=1/../y")).isEqualTo("http://a/b/c/y"); + + assertThat(resolve(base, "g?y/./x")).isEqualTo("http://a/b/c/g?y/./x"); + assertThat(resolve(base, "g?y/../x")).isEqualTo("http://a/b/c/g?y/../x"); + assertThat(resolve(base, "g#s/./x")).isEqualTo("http://a/b/c/g#s/./x"); + assertThat(resolve(base, "g#s/../x")).isEqualTo("http://a/b/c/g#s/../x"); + + assertThat(resolve(base, "http:g")).isEqualTo("http:g"); + } + + /** + * Tests additional abnormal usage of {@link UriUtil#resolve(String, String)}. + */ + @Test + public void testResolveAbnormalAdditional() { + assertThat(resolve("http://a/b", "c:d/../e")).isEqualTo("c:e"); + assertThat(resolve("a:b", "../c")).isEqualTo("a:c"); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java new file mode 100644 index 0000000000..70caff9bf1 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -0,0 +1,200 @@ +/* + * 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.util; + +import static com.google.android.exoplayer2.util.Util.binarySearchCeil; +import static com.google.android.exoplayer2.util.Util.binarySearchFloor; +import static com.google.android.exoplayer2.util.Util.escapeFileName; +import static com.google.android.exoplayer2.util.Util.parseXsDateTime; +import static com.google.android.exoplayer2.util.Util.parseXsDuration; +import static com.google.android.exoplayer2.util.Util.unescapeFileName; +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.testutil.TestUtil; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit tests for {@link Util}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class UtilTest { + + @Test + public void testArrayBinarySearchFloor() { + long[] values = new long[0]; + assertThat(binarySearchFloor(values, 0, false, false)).isEqualTo(-1); + assertThat(binarySearchFloor(values, 0, false, true)).isEqualTo(0); + + values = new long[] {1, 3, 5}; + assertThat(binarySearchFloor(values, 0, false, false)).isEqualTo(-1); + assertThat(binarySearchFloor(values, 0, true, false)).isEqualTo(-1); + assertThat(binarySearchFloor(values, 0, false, true)).isEqualTo(0); + assertThat(binarySearchFloor(values, 0, true, true)).isEqualTo(0); + + assertThat(binarySearchFloor(values, 1, false, false)).isEqualTo(-1); + assertThat(binarySearchFloor(values, 1, true, false)).isEqualTo(0); + assertThat(binarySearchFloor(values, 1, false, true)).isEqualTo(0); + assertThat(binarySearchFloor(values, 1, true, true)).isEqualTo(0); + + assertThat(binarySearchFloor(values, 4, false, false)).isEqualTo(1); + assertThat(binarySearchFloor(values, 4, true, false)).isEqualTo(1); + + assertThat(binarySearchFloor(values, 5, false, false)).isEqualTo(1); + assertThat(binarySearchFloor(values, 5, true, false)).isEqualTo(2); + + assertThat(binarySearchFloor(values, 6, false, false)).isEqualTo(2); + assertThat(binarySearchFloor(values, 6, true, false)).isEqualTo(2); + } + + @Test + public void testListBinarySearchFloor() { + List values = new ArrayList<>(); + assertThat(binarySearchFloor(values, 0, false, false)).isEqualTo(-1); + assertThat(binarySearchFloor(values, 0, false, true)).isEqualTo(0); + + values.add(1); + values.add(3); + values.add(5); + assertThat(binarySearchFloor(values, 0, false, false)).isEqualTo(-1); + assertThat(binarySearchFloor(values, 0, true, false)).isEqualTo(-1); + assertThat(binarySearchFloor(values, 0, false, true)).isEqualTo(0); + assertThat(binarySearchFloor(values, 0, true, true)).isEqualTo(0); + + assertThat(binarySearchFloor(values, 1, false, false)).isEqualTo(-1); + assertThat(binarySearchFloor(values, 1, true, false)).isEqualTo(0); + assertThat(binarySearchFloor(values, 1, false, true)).isEqualTo(0); + assertThat(binarySearchFloor(values, 1, true, true)).isEqualTo(0); + + assertThat(binarySearchFloor(values, 4, false, false)).isEqualTo(1); + assertThat(binarySearchFloor(values, 4, true, false)).isEqualTo(1); + + assertThat(binarySearchFloor(values, 5, false, false)).isEqualTo(1); + assertThat(binarySearchFloor(values, 5, true, false)).isEqualTo(2); + + assertThat(binarySearchFloor(values, 6, false, false)).isEqualTo(2); + assertThat(binarySearchFloor(values, 6, true, false)).isEqualTo(2); + } + + @Test + public void testArrayBinarySearchCeil() { + long[] values = new long[0]; + assertThat(binarySearchCeil(values, 0, false, false)).isEqualTo(0); + assertThat(binarySearchCeil(values, 0, false, true)).isEqualTo(-1); + + values = new long[] {1, 3, 5}; + assertThat(binarySearchCeil(values, 0, false, false)).isEqualTo(0); + assertThat(binarySearchCeil(values, 0, true, false)).isEqualTo(0); + + assertThat(binarySearchCeil(values, 1, false, false)).isEqualTo(1); + assertThat(binarySearchCeil(values, 1, true, false)).isEqualTo(0); + + assertThat(binarySearchCeil(values, 2, false, false)).isEqualTo(1); + assertThat(binarySearchCeil(values, 2, true, false)).isEqualTo(1); + + assertThat(binarySearchCeil(values, 5, false, false)).isEqualTo(3); + assertThat(binarySearchCeil(values, 5, true, false)).isEqualTo(2); + assertThat(binarySearchCeil(values, 5, false, true)).isEqualTo(2); + assertThat(binarySearchCeil(values, 5, true, true)).isEqualTo(2); + + assertThat(binarySearchCeil(values, 6, false, false)).isEqualTo(3); + assertThat(binarySearchCeil(values, 6, true, false)).isEqualTo(3); + assertThat(binarySearchCeil(values, 6, false, true)).isEqualTo(2); + assertThat(binarySearchCeil(values, 6, true, true)).isEqualTo(2); + } + + @Test + public void testListBinarySearchCeil() { + List values = new ArrayList<>(); + assertThat(binarySearchCeil(values, 0, false, false)).isEqualTo(0); + assertThat(binarySearchCeil(values, 0, false, true)).isEqualTo(-1); + + values.add(1); + values.add(3); + values.add(5); + assertThat(binarySearchCeil(values, 0, false, false)).isEqualTo(0); + assertThat(binarySearchCeil(values, 0, true, false)).isEqualTo(0); + + assertThat(binarySearchCeil(values, 1, false, false)).isEqualTo(1); + assertThat(binarySearchCeil(values, 1, true, false)).isEqualTo(0); + + assertThat(binarySearchCeil(values, 2, false, false)).isEqualTo(1); + assertThat(binarySearchCeil(values, 2, true, false)).isEqualTo(1); + + assertThat(binarySearchCeil(values, 5, false, false)).isEqualTo(3); + assertThat(binarySearchCeil(values, 5, true, false)).isEqualTo(2); + assertThat(binarySearchCeil(values, 5, false, true)).isEqualTo(2); + assertThat(binarySearchCeil(values, 5, true, true)).isEqualTo(2); + + assertThat(binarySearchCeil(values, 6, false, false)).isEqualTo(3); + assertThat(binarySearchCeil(values, 6, true, false)).isEqualTo(3); + assertThat(binarySearchCeil(values, 6, false, true)).isEqualTo(2); + assertThat(binarySearchCeil(values, 6, true, true)).isEqualTo(2); + } + + @Test + public void testParseXsDuration() { + assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L); + assertThat(parseXsDuration("PT1.500S")).isEqualTo(1500L); + } + + @Test + public void testParseXsDateTime() throws Exception { + assertThat(parseXsDateTime("2014-06-19T23:07:42")).isEqualTo(1403219262000L); + assertThat(parseXsDateTime("2014-08-06T11:00:00Z")).isEqualTo(1407322800000L); + assertThat(parseXsDateTime("2014-08-06T11:00:00,000Z")).isEqualTo(1407322800000L); + assertThat(parseXsDateTime("2014-09-19T13:18:55-08:00")).isEqualTo(1411161535000L); + assertThat(parseXsDateTime("2014-09-19T13:18:55-0800")).isEqualTo(1411161535000L); + assertThat(parseXsDateTime("2014-09-19T13:18:55.000-0800")).isEqualTo(1411161535000L); + assertThat(parseXsDateTime("2014-09-19T13:18:55.000-800")).isEqualTo(1411161535000L); + } + + @Test + public void testUnescapeInvalidFileName() { + assertThat(Util.unescapeFileName("%a")).isNull(); + assertThat(Util.unescapeFileName("%xyz")).isNull(); + } + + @Test + public void testEscapeUnescapeFileName() { + assertEscapeUnescapeFileName("just+a regular+fileName", "just+a regular+fileName"); + assertEscapeUnescapeFileName("key:value", "key%3avalue"); + assertEscapeUnescapeFileName("<>:\"/\\|?*%", "%3c%3e%3a%22%2f%5c%7c%3f%2a%25"); + + Random random = new Random(0); + for (int i = 0; i < 1000; i++) { + String string = TestUtil.buildTestString(1000, random); + assertEscapeUnescapeFileName(string); + } + } + + private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) { + assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName); + assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); + } + + private static void assertEscapeUnescapeFileName(String fileName) { + String escapedFileName = Util.escapeFileName(fileName); + assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); + } + +} From ca2bfbc56ea91169e2abdb56170340cf28e8e9f7 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 7 Apr 2017 12:10:08 +0100 Subject: [PATCH 0043/1327] DashManifestParser: Move schemeType up to DrmInitData ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167171407 --- .../exoplayer2/drm/DrmInitDataTest.java | 10 +-- .../drm/OfflineLicenseHelperTest.java | 2 +- .../drm/DefaultDrmSessionManager.java | 2 +- .../android/exoplayer2/drm/DrmInitData.java | 77 +++++++------------ .../extractor/mkv/MatroskaExtractor.java | 2 +- .../extractor/mp4/FragmentedMp4Extractor.java | 2 +- .../google/android/exoplayer2/FormatTest.java | 6 +- .../exoplayer2/source/dash/DashUtilTest.java | 2 +- .../dash/manifest/DashManifestParser.java | 62 ++++++++++----- .../manifest/SsManifestParser.java | 2 +- 10 files changed, 81 insertions(+), 86 deletions(-) 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 aa8cbfdb62..8a2c24beba 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,15 +31,15 @@ import junit.framework.TestCase; */ public class DrmInitDataTest extends TestCase { - private static final SchemeData DATA_1 = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4, + 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, null, VIDEO_MP4, + 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, "cbc1", VIDEO_MP4, + 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, null, VIDEO_MP4, + 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, null, VIDEO_MP4, + private static final SchemeData DATA_UNIVERSAL = new SchemeData(C.UUID_NIL, VIDEO_MP4, TestUtil.buildTestData(128, 3 /* data seed */)); public void testParcelable() { 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 43c867f435..821656475d 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 @@ -153,7 +153,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { } private static DrmInitData newDrmInitData() { - return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "cenc", "mimeType", + return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType", new byte[] {1, 4, 7, 0, 3, 6})); } 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 25a73a67c1..9ea696e074 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 @@ -278,7 +278,7 @@ public class DefaultDrmSessionManager implements DrmSe // No data for this manager's scheme. return false; } - String schemeType = schemeData.type; + String schemeType = drmInitData.schemeType; if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) { // If there is no scheme information, assume patternless AES-CTR. return true; 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 d814ece31d..c8e76ec291 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 @@ -37,6 +37,11 @@ public final class DrmInitData implements Comparator, Parcelable { // Lazily initialized hashcode. private int hashCode; + /** + * The protection scheme type, or null if not applicable or unknown. + */ + @Nullable public final String schemeType; + /** * Number of {@link SchemeData}s. */ @@ -46,17 +51,19 @@ public final class DrmInitData implements Comparator, Parcelable { * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. */ public DrmInitData(List schemeDatas) { - this(false, schemeDatas.toArray(new SchemeData[schemeDatas.size()])); + this(null, false, schemeDatas.toArray(new SchemeData[schemeDatas.size()])); } /** * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. */ public DrmInitData(SchemeData... schemeDatas) { - this(true, schemeDatas); + this(null, true, schemeDatas); } - private DrmInitData(boolean cloneSchemeDatas, SchemeData... schemeDatas) { + private DrmInitData(@Nullable String schemeType, boolean cloneSchemeDatas, + SchemeData... schemeDatas) { + this.schemeType = schemeType; if (cloneSchemeDatas) { schemeDatas = schemeDatas.clone(); } @@ -74,6 +81,7 @@ public final class DrmInitData implements Comparator, Parcelable { } /* package */ DrmInitData(Parcel in) { + schemeType = in.readString(); schemeDatas = in.createTypedArray(SchemeData.CREATOR); schemeDataCount = schemeDatas.length; } @@ -104,36 +112,24 @@ public final class DrmInitData implements Comparator, Parcelable { } /** - * Returns a copy of the {@link DrmInitData} instance whose {@link SchemeData}s have been updated - * to have the specified scheme type. + * Returns a copy with the specified protection 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. + * @return A copy with the specified protection 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 { + if (Util.areEqual(this.schemeType, schemeType)) { return this; } + return new DrmInitData(schemeType, false, schemeDatas); } @Override public int hashCode() { if (hashCode == 0) { - hashCode = Arrays.hashCode(schemeDatas); + int result = (schemeType == null ? 0 : schemeType.hashCode()); + result = 31 * result + Arrays.hashCode(schemeDatas); + hashCode = result; } return hashCode; } @@ -146,7 +142,9 @@ public final class DrmInitData implements Comparator, Parcelable { if (obj == null || getClass() != obj.getClass()) { return false; } - return Arrays.equals(schemeDatas, ((DrmInitData) obj).schemeDatas); + DrmInitData other = (DrmInitData) obj; + return Util.areEqual(schemeType, other.schemeType) + && Arrays.equals(schemeDatas, other.schemeDatas); } @Override @@ -164,6 +162,7 @@ public final class DrmInitData implements Comparator, Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeString(schemeType); dest.writeTypedArray(schemeDatas, 0); } @@ -195,10 +194,6 @@ 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}. */ @@ -215,26 +210,22 @@ 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, @Nullable String type, String mimeType, byte[] data) { - this(uuid, type, mimeType, data, false); + public SchemeData(UUID uuid, String mimeType, byte[] data) { + this(uuid, 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, @Nullable String type, String mimeType, byte[] data, - boolean requiresSecureDecryption) { + public SchemeData(UUID uuid, 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; @@ -242,7 +233,6 @@ 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; @@ -258,19 +248,6 @@ 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)) { @@ -281,14 +258,13 @@ public final class DrmInitData implements Comparator, Parcelable { } SchemeData other = (SchemeData) obj; return mimeType.equals(other.mimeType) && Util.areEqual(uuid, other.uuid) - && Util.areEqual(type, other.type) && Arrays.equals(data, other.data); + && 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; @@ -307,7 +283,6 @@ 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 6c4eb033ce..a5fe6ae35b 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 @@ -628,7 +628,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, null, + currentTrack.drmInitData = new DrmInitData(new SchemeData(C.UUID_NIL, MimeTypes.VIDEO_WEBM, currentTrack.cryptoData.encryptionKey)); } break; 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 c3f2a9fb38..d9ab47546a 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 @@ -1275,7 +1275,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, null, MimeTypes.VIDEO_MP4, psshData)); + schemeDatas.add(new SchemeData(uuid, MimeTypes.VIDEO_MP4, psshData)); } } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java b/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java index 8e36edc105..33e1a673bd 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java @@ -60,11 +60,11 @@ public final class FormatTest { @Test public void testParcelable() { - DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, "cenc", VIDEO_MP4, + DrmInitData.SchemeData drmData1 = new DrmInitData.SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */)); - DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, null, VIDEO_WEBM, + DrmInitData.SchemeData drmData2 = new DrmInitData.SchemeData(C.UUID_NIL, VIDEO_WEBM, TestUtil.buildTestData(128, 1 /* data seed */)); - DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2); + DrmInitData drmInitData = new DrmInitData(drmData1, drmData2); byte[] projectionData = new byte[] {1, 2, 3}; Metadata metadata = new Metadata( new TextInformationFrame("id1", "description1", "value1"), 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 7a1c78b2e8..3db8c6b2f9 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, null, "mimeType", + return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "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 2f4724c258..c973db79d7 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 @@ -238,6 +238,7 @@ public class DashManifestParser extends DefaultHandler int audioChannels = Format.NO_VALUE; int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE); String language = xpp.getAttributeValue(null, "lang"); + String drmSchemeType = null; ArrayList drmSchemeDatas = new ArrayList<>(); ArrayList inbandEventStreams = new ArrayList<>(); ArrayList accessibilityDescriptors = new ArrayList<>(); @@ -254,9 +255,12 @@ public class DashManifestParser extends DefaultHandler seenFirstBaseUrl = true; } } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { - SchemeData contentProtection = parseContentProtection(xpp); - if (contentProtection != null) { - drmSchemeDatas.add(contentProtection); + Pair contentProtection = parseContentProtection(xpp); + if (contentProtection.first != null) { + drmSchemeType = contentProtection.first; + } + if (contentProtection.second != null) { + drmSchemeDatas.add(contentProtection.second); } } else if (XmlPullParserUtil.isStartTag(xpp, "ContentComponent")) { language = checkLanguageConsistency(language, xpp.getAttributeValue(null, "lang")); @@ -293,7 +297,7 @@ public class DashManifestParser extends DefaultHandler List representations = new ArrayList<>(representationInfos.size()); for (int i = 0; i < representationInfos.size(); i++) { representations.add(buildRepresentation(representationInfos.get(i), contentId, - drmSchemeDatas, inbandEventStreams)); + drmSchemeType, drmSchemeDatas, inbandEventStreams)); } return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors, @@ -311,9 +315,9 @@ public class DashManifestParser extends DefaultHandler String contentType = xpp.getAttributeValue(null, "contentType"); return TextUtils.isEmpty(contentType) ? C.TRACK_TYPE_UNKNOWN : MimeTypes.BASE_TYPE_AUDIO.equals(contentType) ? C.TRACK_TYPE_AUDIO - : MimeTypes.BASE_TYPE_VIDEO.equals(contentType) ? C.TRACK_TYPE_VIDEO - : MimeTypes.BASE_TYPE_TEXT.equals(contentType) ? C.TRACK_TYPE_TEXT - : C.TRACK_TYPE_UNKNOWN; + : MimeTypes.BASE_TYPE_VIDEO.equals(contentType) ? C.TRACK_TYPE_VIDEO + : MimeTypes.BASE_TYPE_TEXT.equals(contentType) ? C.TRACK_TYPE_TEXT + : C.TRACK_TYPE_UNKNOWN; } protected int getContentType(Format format) { @@ -336,19 +340,20 @@ public class DashManifestParser extends DefaultHandler * @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 {@link SchemeData} parsed from the ContentProtection element, or null if the element is - * unsupported. + * @return The scheme type and/or {@link SchemeData} parsed from the ContentProtection element. + * Either or both may be null, depending on the ContentProtection element being parsed. */ - protected SchemeData parseContentProtection(XmlPullParser xpp) throws XmlPullParserException, - IOException { + protected Pair parseContentProtection(XmlPullParser xpp) + throws XmlPullParserException, IOException { String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); boolean isPlayReady = "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95".equals(schemeIdUri); - String schemeType = xpp.getAttributeValue(null, "value"); + String schemeType = null; byte[] data = null; UUID uuid = null; boolean requiresSecureDecoder = false; if ("urn:mpeg:dash:mp4protection:2011".equals(schemeIdUri)) { + schemeType = xpp.getAttributeValue(null, "value"); String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); if (defaultKid != null) { UUID keyId = UUID.fromString(defaultKid); @@ -379,8 +384,9 @@ public class DashManifestParser extends DefaultHandler requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); } } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); - return data != null - ? new SchemeData(uuid, schemeType, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) : null; + SchemeData schemeData = data != null + ? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) : null; + return Pair.create(schemeType, schemeData); } /** @@ -432,6 +438,7 @@ public class DashManifestParser extends DefaultHandler float frameRate = parseFrameRate(xpp, adaptationSetFrameRate); int audioChannels = adaptationSetAudioChannels; int audioSamplingRate = parseInt(xpp, "audioSamplingRate", adaptationSetAudioSamplingRate); + String drmSchemeType = null; ArrayList drmSchemeDatas = new ArrayList<>(); ArrayList inbandEventStreams = new ArrayList<>(); @@ -452,9 +459,12 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { - SchemeData contentProtection = parseContentProtection(xpp); - if (contentProtection != null) { - drmSchemeDatas.add(contentProtection); + Pair contentProtection = parseContentProtection(xpp); + if (contentProtection.first != null) { + drmSchemeType = contentProtection.first; + } + if (contentProtection.second != null) { + drmSchemeDatas.add(contentProtection.second); } } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); @@ -466,7 +476,8 @@ public class DashManifestParser extends DefaultHandler adaptationSetAccessibilityDescriptors, codecs); segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(); - return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeDatas, inbandEventStreams); + return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeType, drmSchemeDatas, + inbandEventStreams); } protected Format buildFormat(String id, String containerMimeType, int width, int height, @@ -499,13 +510,19 @@ public class DashManifestParser extends DefaultHandler } protected Representation buildRepresentation(RepresentationInfo representationInfo, - String contentId, ArrayList extraDrmSchemeDatas, + String contentId, String extraDrmSchemeType, ArrayList extraDrmSchemeDatas, ArrayList extraInbandEventStreams) { Format format = representationInfo.format; + String drmSchemeType = representationInfo.drmSchemeType != null + ? representationInfo.drmSchemeType : extraDrmSchemeType; ArrayList drmSchemeDatas = representationInfo.drmSchemeDatas; drmSchemeDatas.addAll(extraDrmSchemeDatas); if (!drmSchemeDatas.isEmpty()) { - format = format.copyWithDrmInitData(new DrmInitData(drmSchemeDatas)); + DrmInitData drmInitData = new DrmInitData(drmSchemeDatas); + if (drmSchemeType != null) { + drmInitData = drmInitData.copyWithSchemeType(drmSchemeType); + } + format = format.copyWithDrmInitData(drmInitData); } ArrayList inbandEventStremas = representationInfo.inbandEventStreams; inbandEventStremas.addAll(extraInbandEventStreams); @@ -946,14 +963,17 @@ public class DashManifestParser extends DefaultHandler public final Format format; public final String baseUrl; public final SegmentBase segmentBase; + public final String drmSchemeType; public final ArrayList drmSchemeDatas; public final ArrayList inbandEventStreams; public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase, - ArrayList drmSchemeDatas, ArrayList inbandEventStreams) { + String drmSchemeType, ArrayList drmSchemeDatas, + ArrayList inbandEventStreams) { this.format = format; this.baseUrl = baseUrl; this.segmentBase = segmentBase; + this.drmSchemeType = drmSchemeType; this.drmSchemeDatas = drmSchemeDatas; this.inbandEventStreams = inbandEventStreams; } 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 5784cc7bc6..3ca5f8d997 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, null, + DrmInitData drmInitData = new DrmInitData(new SchemeData(protectionElement.uuid, MimeTypes.VIDEO_MP4, protectionElement.data)); for (StreamElement streamElement : streamElementArray) { for (int i = 0; i < streamElement.formats.length; i++) { From 7804c2b07961390d7a91d788ad2883e5dfbbfdfd Mon Sep 17 00:00:00 2001 From: anjalibh Date: Fri, 1 Sep 2017 18:02:20 -0700 Subject: [PATCH 0044/1327] HDR 10 bits: Use a separate sampler for U and V dithering. Using the same sampler introduced some minor horizontal scratches. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167347995 --- extensions/vp9/src/main/jni/vpx_jni.cc | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index d02d524713..f0b93b1dc2 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -197,12 +197,12 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { const int32_t uvHeight = (img->d_h + 1) / 2; const uint64_t yLength = img->stride[VPX_PLANE_Y] * img->d_h; const uint64_t uvLength = img->stride[VPX_PLANE_U] * uvHeight; - int sample = 0; if (img->fmt == VPX_IMG_FMT_I42016) { // HBD planar 420. // Note: The stride for BT2020 is twice of what we use so this is wasting // memory. The long term goal however is to upload half-float/short so // it's not important to optimize the stride at this time. // Y + int sampleY = 0; for (int y = 0; y < img->d_h; y++) { const uint16_t* srcBase = reinterpret_cast( img->planes[VPX_PLANE_Y] + img->stride[VPX_PLANE_Y] * y); @@ -210,12 +210,14 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { for (int x = 0; x < img->d_w; x++) { // Lightweight dither. Carryover the remainder of each 10->8 bit // conversion to the next pixel. - sample += *srcBase++; - *destBase++ = sample >> 2; - sample = sample & 3; // Remainder. + sampleY += *srcBase++; + *destBase++ = sampleY >> 2; + sampleY = sampleY & 3; // Remainder. } } // UV + int sampleU = 0; + int sampleV = 0; const int32_t uvWidth = (img->d_w + 1) / 2; for (int y = 0; y < uvHeight; y++) { const uint16_t* srcUBase = reinterpret_cast( @@ -228,11 +230,12 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { for (int x = 0; x < uvWidth; x++) { // Lightweight dither. Carryover the remainder of each 10->8 bit // conversion to the next pixel. - sample += *srcUBase++; - *destUBase++ = sample >> 2; - sample = (*srcVBase++) + (sample & 3); // srcV + previousRemainder. - *destVBase++ = sample >> 2; - sample = sample & 3; // Remainder. + sampleU += *srcUBase++; + *destUBase++ = sampleU >> 2; + sampleU = sampleU & 3; // Remainder. + sampleV += *srcVBase++; + *destVBase++ = sampleV >> 2; + sampleV = sampleV & 3; // Remainder. } } } else { From ba0bd729192f6f99e80cc11d647e3ddc41977dda Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Sep 2017 02:11:00 -0700 Subject: [PATCH 0045/1327] Fix typo ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167474040 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aff473c488..bd261e75ff 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ 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) +compile project(':exoplayer-library-ui') ``` ## Developing ExoPlayer ## From 74b8c45e6dd9c13ed9a32a1aad1ce795535b2ac2 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 4 Sep 2017 11:34:57 +0100 Subject: [PATCH 0046/1327] Minor cleanup to AspectRatioFrameLayout --- .../exoplayer2/ui/AspectRatioFrameLayout.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index 3367a46374..037519b7a4 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -32,7 +32,8 @@ public final class AspectRatioFrameLayout extends FrameLayout { * Resize modes for {@link AspectRatioFrameLayout}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_ZOOM}) + @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, + RESIZE_MODE_ZOOM}) public @interface ResizeMode {} /** @@ -52,7 +53,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { */ public static final int RESIZE_MODE_FILL = 3; /** - * Either height or width is increased to obtain the desired aspect ratio. + * Either the width or height is increased to obtain the desired aspect ratio. */ public static final int RESIZE_MODE_ZOOM = 4; @@ -89,7 +90,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { } /** - * Set the aspect ratio that this view should satisfy. + * Sets the aspect ratio that this view should satisfy. * * @param widthHeightRatio The width to height ratio. */ @@ -101,12 +102,10 @@ public final class AspectRatioFrameLayout extends FrameLayout { } /** - * Gets the resize mode. - * - * @return The resize mode. + * Returns the resize mode. */ - public int getResizeMode() { - return this.resizeMode; + public @ResizeMode int getResizeMode() { + return resizeMode; } /** @@ -146,7 +145,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { width = (int) (height * videoAspectRatio); break; case RESIZE_MODE_ZOOM: - if (videoAspectRatio > viewAspectRatio) { + if (aspectDeformation > 0) { width = (int) (height * videoAspectRatio); } else { height = (int) (width / videoAspectRatio); From ab1e4df11aefe71c8069730ad5a9300733440f25 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Sep 2017 05:32:54 -0700 Subject: [PATCH 0047/1327] Update moe eqiuvalence ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167488837 --- .../android/exoplayer2/video/DummySurface.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 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 8551f2541d..a1820ed7a1 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 @@ -43,7 +43,6 @@ 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; @@ -153,16 +152,9 @@ public final class DummySurface extends Surface { * * @param context Any {@link Context}. */ - @SuppressWarnings("unused") + @SuppressWarnings("unused") // Context may be needed in the future for better targeting. private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { - return (Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER)) - || (Util.SDK_INT >= 24 && Util.SDK_INT < 26 - && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager())); - } - - @TargetApi(24) - private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) { - return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE); + return Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER); } private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, From bab2ce817e76431243aab61cb40425d163c3141d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Sep 2017 06:47:03 -0700 Subject: [PATCH 0048/1327] Allow EXIF tracks to be exposed ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167493800 --- .../main/java/com/google/android/exoplayer2/util/MimeTypes.java | 1 + 1 file changed, 1 insertion(+) 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 1c8bb62a75..2daf16d3d2 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 @@ -85,6 +85,7 @@ public final class MimeTypes { public static final String APPLICATION_CAMERA_MOTION = BASE_TYPE_APPLICATION + "/x-camera-motion"; public static final String APPLICATION_EMSG = BASE_TYPE_APPLICATION + "/x-emsg"; public static final String APPLICATION_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs"; + public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif"; private MimeTypes() {} From 0d86f4475c4b30e5a1a0ac55a5479ca680fc2096 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 4 Sep 2017 06:55:07 -0700 Subject: [PATCH 0049/1327] Remove the resampling to 16bit step from FlacDecoder. Currently FlacDecoder/FlacExtractor always perform resampling to 16bit. In some case (with 24bit audio), this might lower the audio quality if the system supports 24bit audio. Since AudioTrack implementation supports resampling, we will remove the resampling step, and return an output with the same bits-per-sample as the original stream. User can choose to re-sample to 16bit in AudioTrack if necessary. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167494350 --- extensions/flac/README.md | 2 +- .../exoplayer2/ext/flac/FlacExtractor.java | 20 ++- extensions/flac/src/main/jni/flac_parser.cc | 124 ++++++------------ .../flac/src/main/jni/include/flac_parser.h | 4 +- .../exoplayer2/util/FlacStreamInfo.java | 2 +- 5 files changed, 57 insertions(+), 95 deletions(-) diff --git a/extensions/flac/README.md b/extensions/flac/README.md index cd0f2efe47..fda5f0085d 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -38,7 +38,7 @@ NDK_PATH="" ``` cd "${FLAC_EXT_PATH}/jni" && \ -curl http://downloads.xiph.org/releases/flac/flac-1.3.1.tar.xz | tar xJ && \ +curl https://ftp.osuosl.org/pub/xiph/releases/flac/flac-1.3.1.tar.xz | tar xJ && \ mv flac-1.3.1 flac ``` diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 7b71b5c743..a2f141a712 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.ext.flac; +import static com.google.android.exoplayer2.util.Util.getPcmEncoding; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; @@ -122,10 +124,20 @@ public final class FlacExtractor implements Extractor { } }); - - Format mediaFormat = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, - streamInfo.bitRate(), Format.NO_VALUE, streamInfo.channels, streamInfo.sampleRate, - C.ENCODING_PCM_16BIT, null, null, 0, null); + Format mediaFormat = + Format.createAudioSampleFormat( + null, + MimeTypes.AUDIO_RAW, + null, + streamInfo.bitRate(), + Format.NO_VALUE, + streamInfo.channels, + streamInfo.sampleRate, + getPcmEncoding(streamInfo.bitsPerSample), + null, + null, + 0, + null); trackOutput.format(mediaFormat); outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index 6c6e57f5f7..b9918e7871 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -42,6 +42,9 @@ #define CHECK(x) \ if (!(x)) ALOGE("Check failed: %s ", #x) +const int endian = 1; +#define isBigEndian() (*(reinterpret_cast(&endian)) == 0) + // The FLAC parser calls our C++ static callbacks using C calling conventions, // inside FLAC__stream_decoder_process_until_end_of_metadata // and FLAC__stream_decoder_process_single. @@ -180,85 +183,42 @@ void FLACParser::errorCallback(FLAC__StreamDecoderErrorStatus status) { mErrorStatus = status; } -// Copy samples from FLAC native 32-bit non-interleaved to 16-bit interleaved. +// Copy samples from FLAC native 32-bit non-interleaved to +// correct bit-depth (non-zero padded), interleaved. // These are candidates for optimization if needed. - -static void copyMono8(int16_t *dst, const int *const *src, unsigned nSamples, - unsigned /* nChannels */) { - for (unsigned i = 0; i < nSamples; ++i) { - *dst++ = src[0][i] << 8; - } -} - -static void copyStereo8(int16_t *dst, const int *const *src, unsigned nSamples, - unsigned /* nChannels */) { - for (unsigned i = 0; i < nSamples; ++i) { - *dst++ = src[0][i] << 8; - *dst++ = src[1][i] << 8; - } -} - -static void copyMultiCh8(int16_t *dst, const int *const *src, unsigned nSamples, - unsigned nChannels) { +static void copyToByteArrayBigEndian(int8_t *dst, const int *const *src, + unsigned bytesPerSample, unsigned nSamples, + unsigned nChannels) { for (unsigned i = 0; i < nSamples; ++i) { for (unsigned c = 0; c < nChannels; ++c) { - *dst++ = src[c][i] << 8; + // point to the first byte of the source address + // and then skip the first few bytes (most significant bytes) + // depending on the bit depth + const int8_t *byteSrc = + reinterpret_cast(&src[c][i]) + 4 - bytesPerSample; + memcpy(dst, byteSrc, bytesPerSample); + dst = dst + bytesPerSample; } } } -static void copyMono16(int16_t *dst, const int *const *src, unsigned nSamples, - unsigned /* nChannels */) { +static void copyToByteArrayLittleEndian(int8_t *dst, const int *const *src, + unsigned bytesPerSample, + unsigned nSamples, unsigned nChannels) { for (unsigned i = 0; i < nSamples; ++i) { - *dst++ = src[0][i]; + for (unsigned c = 0; c < nChannels; ++c) { + // with little endian, the most significant bytes will be at the end + // copy the bytes in little endian will remove the most significant byte + // so we are good here. + memcpy(dst, &(src[c][i]), bytesPerSample); + dst = dst + bytesPerSample; + } } } -static void copyStereo16(int16_t *dst, const int *const *src, unsigned nSamples, +static void copyTrespass(int8_t * /* dst */, const int *const * /* src */, + unsigned /* bytesPerSample */, unsigned /* nSamples */, unsigned /* nChannels */) { - for (unsigned i = 0; i < nSamples; ++i) { - *dst++ = src[0][i]; - *dst++ = src[1][i]; - } -} - -static void copyMultiCh16(int16_t *dst, const int *const *src, - unsigned nSamples, unsigned nChannels) { - for (unsigned i = 0; i < nSamples; ++i) { - for (unsigned c = 0; c < nChannels; ++c) { - *dst++ = src[c][i]; - } - } -} - -// 24-bit versions should do dithering or noise-shaping, here or in AudioFlinger - -static void copyMono24(int16_t *dst, const int *const *src, unsigned nSamples, - unsigned /* nChannels */) { - for (unsigned i = 0; i < nSamples; ++i) { - *dst++ = src[0][i] >> 8; - } -} - -static void copyStereo24(int16_t *dst, const int *const *src, unsigned nSamples, - unsigned /* nChannels */) { - for (unsigned i = 0; i < nSamples; ++i) { - *dst++ = src[0][i] >> 8; - *dst++ = src[1][i] >> 8; - } -} - -static void copyMultiCh24(int16_t *dst, const int *const *src, - unsigned nSamples, unsigned nChannels) { - for (unsigned i = 0; i < nSamples; ++i) { - for (unsigned c = 0; c < nChannels; ++c) { - *dst++ = src[c][i] >> 8; - } - } -} - -static void copyTrespass(int16_t * /* dst */, const int *const * /* src */, - unsigned /* nSamples */, unsigned /* nChannels */) { TRESPASS(); } @@ -340,6 +300,7 @@ bool FLACParser::decodeMetadata() { case 8: case 16: case 24: + case 32: break; default: ALOGE("unsupported bits per sample %u", getBitsPerSample()); @@ -363,23 +324,11 @@ bool FLACParser::decodeMetadata() { ALOGE("unsupported sample rate %u", getSampleRate()); return false; } - // configure the appropriate copy function, defaulting to trespass - static const struct { - unsigned mChannels; - unsigned mBitsPerSample; - void (*mCopy)(int16_t *dst, const int *const *src, unsigned nSamples, - unsigned nChannels); - } table[] = { - {1, 8, copyMono8}, {2, 8, copyStereo8}, {8, 8, copyMultiCh8}, - {1, 16, copyMono16}, {2, 16, copyStereo16}, {8, 16, copyMultiCh16}, - {1, 24, copyMono24}, {2, 24, copyStereo24}, {8, 24, copyMultiCh24}, - }; - for (unsigned i = 0; i < sizeof(table) / sizeof(table[0]); ++i) { - if (table[i].mChannels >= getChannels() && - table[i].mBitsPerSample == getBitsPerSample()) { - mCopy = table[i].mCopy; - break; - } + // configure the appropriate copy function based on device endianness. + if (isBigEndian()) { + mCopy = copyToByteArrayBigEndian; + } else { + mCopy = copyToByteArrayLittleEndian; } } else { ALOGE("missing STREAMINFO"); @@ -424,7 +373,8 @@ size_t FLACParser::readBuffer(void *output, size_t output_size) { return -1; } - size_t bufferSize = blocksize * getChannels() * sizeof(int16_t); + unsigned bytesPerSample = getBitsPerSample() >> 3; + size_t bufferSize = blocksize * getChannels() * bytesPerSample; if (bufferSize > output_size) { ALOGE( "FLACParser::readBuffer not enough space in output buffer " @@ -434,8 +384,8 @@ size_t FLACParser::readBuffer(void *output, size_t output_size) { } // copy PCM from FLAC write buffer to our media buffer, with interleaving. - (*mCopy)(reinterpret_cast(output), mWriteBuffer, blocksize, - getChannels()); + (*mCopy)(reinterpret_cast(output), mWriteBuffer, bytesPerSample, + blocksize, getChannels()); // fill in buffer metadata CHECK(mWriteHeader.number_type == FLAC__FRAME_NUMBER_TYPE_SAMPLE_NUMBER); diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index 8c302adb36..8a769b66d4 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -86,8 +86,8 @@ class FLACParser { private: DataSource *mDataSource; - void (*mCopy)(int16_t *dst, const int *const *src, unsigned nSamples, - unsigned nChannels); + void (*mCopy)(int8_t *dst, const int *const *src, unsigned bytesPerSample, + unsigned nSamples, unsigned nChannels); // handle to underlying libFLAC parser FLAC__StreamDecoder *mDecoder; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java index 6382f1130e..b08f4a31e3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java @@ -65,7 +65,7 @@ public final class FlacStreamInfo { } public int maxDecodedFrameSize() { - return maxBlockSize * channels * 2; + return maxBlockSize * channels * (bitsPerSample / 8); } public int bitRate() { From c9f31a41cd45bfe0a522ee77dfbe1af8ffd97442 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Sep 2017 07:28:24 -0700 Subject: [PATCH 0050/1327] Adding missing license header in IMA build.gradle ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167496569 --- extensions/ima/build.gradle | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index c084ec6bf8..cb69e92990 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -1,3 +1,16 @@ +// 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. apply from: '../../constants.gradle' apply plugin: 'com.android.library' @@ -6,7 +19,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 14 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } From 472df08f0837ebbdcd0b1c7e8269ad816b13269b Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Sep 2017 08:55:20 -0700 Subject: [PATCH 0051/1327] Additional secure DummySurface device exclusions Merge: https://github.com/google/ExoPlayer/pull/3225 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167502127 --- .../google/android/exoplayer2/video/DummySurface.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 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 a1820ed7a1..d623ea33ea 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 @@ -43,6 +43,7 @@ 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; @@ -152,9 +153,15 @@ public final class DummySurface extends Surface { * * @param context Any {@link Context}. */ - @SuppressWarnings("unused") // Context may be needed in the future for better targeting. private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { - return Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER); + return (Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER)) + || (Util.SDK_INT < 26 + && !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, From a0df5bb50a817bf99c335be2479ab9293ec735bf Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Sep 2017 09:26:59 -0700 Subject: [PATCH 0052/1327] Be robust against unexpected EOS in WebvttCueParser Issue: #3228 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167504122 --- .../text/webvtt/WebvttCueParser.java | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index 30c9c8737e..54af4dbf63 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -21,6 +21,7 @@ import android.text.Layout.Alignment; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; +import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; import android.text.style.AlignmentSpan; import android.text.style.BackgroundColorSpan; @@ -92,19 +93,24 @@ import java.util.regex.Pattern; /* package */ boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder, List styles) { String firstLine = webvttData.readLine(); + if (firstLine == null) { + return false; + } Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine); if (cueHeaderMatcher.matches()) { // We have found the timestamps in the first line. No id present. return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles); - } else { - // The first line is not the timestamps, but could be the cue id. - String secondLine = webvttData.readLine(); - cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine); - if (cueHeaderMatcher.matches()) { - // We can do the rest of the parsing, including the id. - return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder, - styles); - } + } + // The first line is not the timestamps, but could be the cue id. + String secondLine = webvttData.readLine(); + if (secondLine == null) { + return false; + } + cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine); + if (cueHeaderMatcher.matches()) { + // We can do the rest of the parsing, including the id. + return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder, + styles); } return false; } @@ -233,7 +239,7 @@ import java.util.regex.Pattern; // Parse the cue text. textBuilder.setLength(0); String line; - while ((line = webvttData.readLine()) != null && !line.isEmpty()) { + while (!TextUtils.isEmpty(line = webvttData.readLine())) { if (textBuilder.length() > 0) { textBuilder.append("\n"); } From bec5e6e2b2b59290963f7a22a6d005c8dac467e5 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Sep 2017 09:57:56 -0700 Subject: [PATCH 0053/1327] Rewrite logic for enabling secure DummySurface Issue: #3215 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167505797 --- .../exoplayer2/video/DummySurface.java | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 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 d623ea33ea..20fe862dd2 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 @@ -90,12 +90,7 @@ public final class DummySurface extends Surface { */ 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); - } + secureSupported = Util.SDK_INT >= 24 && enableSecureDummySurfaceV24(context); secureSupportedInitialized = true; } return secureSupported; @@ -148,20 +143,28 @@ public final class DummySurface extends Surface { } /** - * Returns whether the device is known to advertise secure surface textures but not implement them - * correctly. + * Returns whether use of secure dummy surfaces should be enabled. * * @param context Any {@link Context}. */ - private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { - return (Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER)) - || (Util.SDK_INT < 26 - && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager())); - } - @TargetApi(24) - private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) { - return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE); + private static boolean enableSecureDummySurfaceV24(Context context) { + EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); + if (eglExtensions == null || !eglExtensions.contains("EGL_EXT_protected_content")) { + // EGL_EXT_protected_content is required to enable secure dummy surfaces. + return false; + } + if (Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER)) { + // Samsung devices running API level 24 are known to be broken [Internal: b/37197802]. + return false; + } + if (Util.SDK_INT < 26 && !context.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) { + // Pre API level 26 devices were not well tested unless they supported VR mode. + return false; + } + return true; } private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, From 264f1c903def8a4f8ec7b65d52478470fc8a6358 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 5 Sep 2017 05:23:22 -0700 Subject: [PATCH 0054/1327] Fix bug in FakeChunkSource. In order to retrieve the data set, the track selection index was used, but the data set is actually indexed by track group indices. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167566049 --- .../google/android/exoplayer2/testutil/FakeChunkSource.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java index b8f25bfbce..41fda178d7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java @@ -92,7 +92,8 @@ public final class FakeChunkSource implements ChunkSource { Format selectedFormat = trackSelection.getSelectedFormat(); long startTimeUs = dataSet.getStartTime(chunkIndex); long endTimeUs = startTimeUs + dataSet.getChunkDuration(chunkIndex); - String uri = dataSet.getUri(trackSelection.getSelectedIndex()); + int trackGroupIndex = trackSelection.getIndexInTrackGroup(trackSelection.getSelectedIndex()); + String uri = dataSet.getUri(trackGroupIndex); Segment fakeDataChunk = dataSet.getData(uri).getSegments().get(chunkIndex); DataSpec dataSpec = new DataSpec(Uri.parse(uri), fakeDataChunk.byteOffset, fakeDataChunk.length, null); From 2f4a3fe1f3a95ed9a8029c6e3d21e537852bc271 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 5 Sep 2017 05:50:43 -0700 Subject: [PATCH 0055/1327] Add postDelayed operation to Clock interface. The default implementation is just calling through to handler.postDelayed, while the fake clock uses its internal time value to trigger the handler calls at the correct time. This is useful to apply a fake clock in situations where a handler is used to post delayed messages. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167567914 --- .../google/android/exoplayer2/util/Clock.java | 12 +++++++ .../android/exoplayer2/util/SystemClock.java | 7 ++++ .../exoplayer2/testutil/FakeClock.java | 34 ++++++++++++++++++- 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java index f8d5759c2c..9619ed53ea 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.util; +import android.os.Handler; + /** * An interface through which system clocks can be read. The {@link #DEFAULT} implementation * must be used for all non-test cases. @@ -36,4 +38,14 @@ public interface Clock { */ void sleep(long sleepTimeMs); + /** + * Post a {@link Runnable} on a {@link Handler} thread with a delay measured by this clock. + * @see Handler#postDelayed(Runnable, long) + * + * @param handler The {@link Handler} to post the {@code runnable} on. + * @param runnable A {@link Runnable} to be posted. + * @param delayMs The delay in milliseconds as measured by this clock. + */ + void postDelayed(Handler handler, Runnable runnable, long delayMs); + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java index 1f937b721b..272c3f43ec 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.util; +import android.os.Handler; + /** * The standard implementation of {@link Clock}. */ @@ -30,4 +32,9 @@ package com.google.android.exoplayer2.util; android.os.SystemClock.sleep(sleepTimeMs); } + @Override + public void postDelayed(Handler handler, Runnable runnable, long delayMs) { + handler.postDelayed(runnable, delayMs); + } + } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java index 36ce4b5c3e..843e5858d8 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.testutil; +import android.os.Handler; import com.google.android.exoplayer2.util.Clock; import java.util.ArrayList; import java.util.List; @@ -26,6 +27,7 @@ public final class FakeClock implements Clock { private long currentTimeMs; private final List wakeUpTimes; + private final List handlerPosts; /** * Create {@link FakeClock} with an arbitrary initial timestamp. @@ -35,6 +37,7 @@ public final class FakeClock implements Clock { public FakeClock(long initialTimeMs) { this.currentTimeMs = initialTimeMs; this.wakeUpTimes = new ArrayList<>(); + this.handlerPosts = new ArrayList<>(); } /** @@ -50,10 +53,16 @@ public final class FakeClock implements Clock { break; } } + for (int i = handlerPosts.size() - 1; i >= 0; i--) { + if (handlerPosts.get(i).postTime <= currentTimeMs) { + HandlerPostData postData = handlerPosts.remove(i); + postData.handler.post(postData.runnable); + } + } } @Override - public long elapsedRealtime() { + public synchronized long elapsedRealtime() { return currentTimeMs; } @@ -74,5 +83,28 @@ public final class FakeClock implements Clock { wakeUpTimes.remove(wakeUpTimeMs); } + @Override + public synchronized void postDelayed(Handler handler, Runnable runnable, long delayMs) { + if (delayMs <= 0) { + handler.post(runnable); + } else { + handlerPosts.add(new HandlerPostData(currentTimeMs + delayMs, handler, runnable)); + } + } + + private static final class HandlerPostData { + + public final long postTime; + public final Handler handler; + public final Runnable runnable; + + public HandlerPostData(long postTime, Handler handler, Runnable runnable) { + this.postTime = postTime; + this.handler = handler; + this.runnable = runnable; + } + + } + } From a90cef0cb5e581757b43264e923e06653cda183e Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Sep 2017 08:03:07 -0700 Subject: [PATCH 0056/1327] Upgrade gradle plugin / wrapper ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167579719 --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index d5cc64baa1..8ec24a6e82 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:2.3.3' + classpath 'com.android.tools.build:gradle:3.0.0-beta4' classpath 'com.novoda:bintray-release:0.5.0' } // Workaround for the following test coverage issue. Remove when fixed: diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fc42154505..32ec7e3327 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Jul 12 10:31:13 BST 2017 +#Tue Sep 05 13:43:42 BST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.0.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip From 0183a83047360f3bf2c60b2180ce356c75b454da Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Sep 2017 08:17:44 -0700 Subject: [PATCH 0057/1327] Don't use MediaCodec.setOutputSurface on Nexus 7 with qcom decoder Issue: #3236 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167581198 --- .../video/MediaCodecVideoRenderer.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index f70d74e413..d72619dbe1 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 @@ -78,6 +78,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private Format[] streamFormats; private CodecMaxValues codecMaxValues; + private boolean codecNeedsSetOutputSurfaceWorkaround; private Surface surface; private Surface dummySurface; @@ -360,7 +361,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { int state = getState(); if (state == STATE_ENABLED || state == STATE_STARTED) { MediaCodec codec = getCodec(); - if (Util.SDK_INT >= 23 && codec != null && surface != null) { + if (Util.SDK_INT >= 23 && codec != null && surface != null + && !codecNeedsSetOutputSurfaceWorkaround) { setOutputSurfaceV23(codec, surface); } else { releaseCodec(); @@ -431,6 +433,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { protected void onCodecInitialized(String name, long initializedTimestampMs, long initializationDurationMs) { eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); + codecNeedsSetOutputSurfaceWorkaround = codecNeedsSetOutputSurfaceWorkaround(name); } @Override @@ -969,6 +972,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER); } + /** + * Returns whether the device is known to implement {@link MediaCodec#setOutputSurface(Surface)} + * incorrectly. + *

    + * If true is returned then we fall back to releasing and re-instantiating the codec instead. + */ + private static boolean codecNeedsSetOutputSurfaceWorkaround(String name) { + // Work around https://github.com/google/ExoPlayer/issues/3236 + return ("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) + && "OMX.qcom.video.decoder.avc".equals(name); + } + /** * Returns whether a codec with suitable {@link CodecMaxValues} will support adaptation between * two {@link Format}s. From 8413dab9dec518a3311762747084fac83d6e03c4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 5 Sep 2017 08:32:56 -0700 Subject: [PATCH 0058/1327] Use clock in action schedule to handle delays. This allows to use a fake clock and an action schedule with timed delays together. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167582860 --- .../exoplayer2/testutil/ActionSchedule.java | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index c9ae02c957..28e62f3057 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -36,6 +36,7 @@ import com.google.android.exoplayer2.testutil.Action.Stop; import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity; import com.google.android.exoplayer2.testutil.Action.WaitForTimelineChanged; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.util.Clock; /** * Schedules a sequence of {@link Action}s for execution during a test. @@ -70,17 +71,27 @@ public final class ActionSchedule { public static final class Builder { private final String tag; + private final Clock clock; private final ActionNode rootNode; - private long currentDelayMs; + private long currentDelayMs; private ActionNode previousNode; /** * @param tag A tag to use for logging. */ public Builder(String tag) { + this(tag, Clock.DEFAULT); + } + + /** + * @param tag A tag to use for logging. + * @param clock A clock to use for measuring delays. + */ + public Builder(String tag, Clock clock) { this.tag = tag; - rootNode = new ActionNode(new RootAction(tag), 0); + this.clock = clock; + rootNode = new ActionNode(new RootAction(tag), clock, 0); previousNode = rootNode; } @@ -102,7 +113,7 @@ public final class ActionSchedule { * @return The builder, for convenience. */ public Builder apply(Action action) { - return appendActionNode(new ActionNode(action, currentDelayMs)); + return appendActionNode(new ActionNode(action, clock, currentDelayMs)); } /** @@ -113,7 +124,7 @@ public final class ActionSchedule { * @return The builder, for convenience. */ public Builder repeat(Action action, long intervalMs) { - return appendActionNode(new ActionNode(action, currentDelayMs, intervalMs)); + return appendActionNode(new ActionNode(action, clock, currentDelayMs, intervalMs)); } /** @@ -274,6 +285,7 @@ public final class ActionSchedule { /* package */ static final class ActionNode implements Runnable { private final Action action; + private final Clock clock; private final long delayMs; private final long repeatIntervalMs; @@ -286,20 +298,23 @@ public final class ActionSchedule { /** * @param action The wrapped action. + * @param clock The clock to use for measuring the delay. * @param delayMs The delay between the node being scheduled and the action being executed. */ - public ActionNode(Action action, long delayMs) { - this(action, delayMs, C.TIME_UNSET); + public ActionNode(Action action, Clock clock, long delayMs) { + this(action, clock, delayMs, C.TIME_UNSET); } /** * @param action The wrapped action. + * @param clock The clock to use for measuring the delay. * @param delayMs The delay between the node being scheduled and the action being executed. * @param repeatIntervalMs The interval between one execution and the next repetition. If set to * {@link C#TIME_UNSET}, the action is executed once only. */ - public ActionNode(Action action, long delayMs, long repeatIntervalMs) { + public ActionNode(Action action, Clock clock, long delayMs, long repeatIntervalMs) { this.action = action; + this.clock = clock; this.delayMs = delayMs; this.repeatIntervalMs = repeatIntervalMs; } @@ -328,14 +343,14 @@ public final class ActionSchedule { this.trackSelector = trackSelector; this.surface = surface; this.mainHandler = mainHandler; - mainHandler.postDelayed(this, delayMs); + clock.postDelayed(mainHandler, this, delayMs); } @Override public void run() { action.doActionAndScheduleNext(player, trackSelector, surface, mainHandler, next); if (repeatIntervalMs != C.TIME_UNSET) { - mainHandler.postDelayed(this, repeatIntervalMs); + clock.postDelayed(mainHandler, this, repeatIntervalMs); } } From e16610a82c696b6121301c792c7f04d5e36a37c7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 5 Sep 2017 08:47:02 -0700 Subject: [PATCH 0059/1327] Fix import formatting ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167584287 --- .../exoplayer2/ext/mediasession/DefaultPlaybackController.java | 1 - .../exoplayer2/ext/mediasession/MediaSessionConnector.java | 1 - .../exoplayer2/ext/mediasession/RepeatModeActionProvider.java | 3 ++- .../exoplayer2/ext/mediasession/TimelineQueueNavigator.java | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java index 95ebcde095..dcb6f8ca10 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ext.mediasession; import android.os.Bundle; import android.os.ResultReceiver; import android.support.v4.media.session.PlaybackStateCompat; - import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.util.RepeatModeUtil; diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 4c7ad123f3..7304d9cdb6 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -40,7 +40,6 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.util.RepeatModeUtil; - import java.util.Collections; import java.util.HashMap; import java.util.List; diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java index b4cb3c73d0..1db5889e00 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java @@ -1,4 +1,3 @@ -package com.google.android.exoplayer2.ext.mediasession; /* * Copyright (c) 2017 The Android Open Source Project * @@ -14,6 +13,8 @@ package com.google.android.exoplayer2.ext.mediasession; * See the License for the specific language governing permissions and * limitations under the License. */ +package com.google.android.exoplayer2.ext.mediasession; + import android.content.Context; import android.os.Bundle; import android.support.v4.media.session.PlaybackStateCompat; diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 0484c0b641..1b9bd3ecd9 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -25,7 +25,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Util; - import java.util.ArrayList; import java.util.Collections; import java.util.List; From b62eab63a4d13945d07431be3d30628392646260 Mon Sep 17 00:00:00 2001 From: zhihuichen Date: Tue, 5 Sep 2017 11:06:03 -0700 Subject: [PATCH 0060/1327] Implement multi session to support DRM key rotation. Spec: https://storage.googleapis.com/wvdocs/Widevine_DRM_Android_Using_Key_Rotation.pdf 1. Implement multisession to support drm key rotation 2. Put MediaDrmEventListener back to manager since this is a per mediaDrm thing. 3. It seems diffrenciate between single/multi session is unnecessary. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167602965 --- .../exoplayer2/demo/PlayerActivity.java | 9 +- .../demo/SampleChooserActivity.java | 63 ++++-- .../exoplayer2/drm/DefaultDrmSession.java | 194 ++++++++---------- .../drm/DefaultDrmSessionManager.java | 185 +++++++++++++++-- 4 files changed, 301 insertions(+), 150 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index b2750a93bb..c2c4df9ea8 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -89,6 +89,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; public static final String DRM_LICENSE_URL = "drm_license_url"; public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties"; + public static final String DRM_MULTI_SESSION = "drm_multi_session"; public static final String PREFER_EXTENSION_DECODERS = "prefer_extension_decoders"; public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; @@ -264,13 +265,14 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi if (drmSchemeUuid != null) { String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); + boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION, false); int errorStringId = R.string.error_drm_unknown; if (Util.SDK_INT < 18) { errorStringId = R.string.error_drm_not_supported; } else { try { drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl, - keyRequestPropertiesArray); + keyRequestPropertiesArray, multiSession); } catch (UnsupportedDrmException e) { errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown; @@ -379,7 +381,8 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi } private DrmSessionManager buildDrmSessionManagerV18(UUID uuid, - String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { + String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession) + throws UnsupportedDrmException { HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, buildHttpDataSourceFactory(false)); if (keyRequestPropertiesArray != null) { @@ -389,7 +392,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi } } return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, - null, mainHandler, eventLogger); + null, mainHandler, eventLogger, multiSession); } private void releasePlayer() { diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index c0edb1d1b8..1f84b1f29c 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -182,6 +182,7 @@ public class SampleChooserActivity extends Activity { UUID drmUuid = null; String drmLicenseUrl = null; String[] drmKeyRequestProperties = null; + boolean drmMultiSession = false; boolean preferExtensionDecoders = false; ArrayList playlistSamples = null; String adTagUri = null; @@ -220,6 +221,9 @@ public class SampleChooserActivity extends Activity { reader.endObject(); drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]); break; + case "drm_multi_session": + drmMultiSession = reader.nextBoolean(); + break; case "prefer_extension_decoders": Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: prefer_extension_decoders"); @@ -242,15 +246,16 @@ public class SampleChooserActivity extends Activity { } } reader.endObject(); - + DrmInfo drmInfo = drmUuid == null ? null : new DrmInfo(drmUuid, drmLicenseUrl, + drmKeyRequestProperties, drmMultiSession); if (playlistSamples != null) { UriSample[] playlistSamplesArray = playlistSamples.toArray( new UriSample[playlistSamples.size()]); - return new PlaylistSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties, - preferExtensionDecoders, playlistSamplesArray); + return new PlaylistSample(sampleName, preferExtensionDecoders, drmInfo, + playlistSamplesArray); } else { - return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties, - preferExtensionDecoders, uri, extension, adTagUri); + return new UriSample(sampleName, preferExtensionDecoders, drmInfo, uri, extension, + adTagUri); } } @@ -372,31 +377,47 @@ public class SampleChooserActivity extends Activity { } - private abstract static class Sample { - - public final String name; - public final boolean preferExtensionDecoders; + private static final class DrmInfo { public final UUID drmSchemeUuid; public final String drmLicenseUrl; public final String[] drmKeyRequestProperties; + public final boolean drmMultiSession; - public Sample(String name, UUID drmSchemeUuid, String drmLicenseUrl, - String[] drmKeyRequestProperties, boolean preferExtensionDecoders) { - this.name = name; + public DrmInfo(UUID drmSchemeUuid, String drmLicenseUrl, + String[] drmKeyRequestProperties, boolean drmMultiSession) { this.drmSchemeUuid = drmSchemeUuid; this.drmLicenseUrl = drmLicenseUrl; this.drmKeyRequestProperties = drmKeyRequestProperties; + this.drmMultiSession = drmMultiSession; + } + + public void updateIntent(Intent intent) { + Assertions.checkNotNull(intent); + intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString()); + intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl); + intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties); + intent.putExtra(PlayerActivity.DRM_MULTI_SESSION, drmMultiSession); + } + } + + private abstract static class Sample { + public final String name; + public final boolean preferExtensionDecoders; + public final DrmInfo drmInfo; + + public Sample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo) { + this.name = name; this.preferExtensionDecoders = preferExtensionDecoders; + this.drmInfo = drmInfo; } public Intent buildIntent(Context context) { Intent intent = new Intent(context, PlayerActivity.class); intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS, preferExtensionDecoders); - if (drmSchemeUuid != null) { - intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString()); - intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl); - intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties); + if (drmInfo != null) { + drmInfo.updateIntent(intent); } + return intent; } @@ -408,10 +429,9 @@ public class SampleChooserActivity extends Activity { public final String extension; public final String adTagUri; - public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, - String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri, + public UriSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo, String uri, String extension, String adTagUri) { - super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders); + super(name, preferExtensionDecoders, drmInfo); this.uri = uri; this.extension = extension; this.adTagUri = adTagUri; @@ -432,10 +452,9 @@ public class SampleChooserActivity extends Activity { public final UriSample[] children; - public PlaylistSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, - String[] drmKeyRequestProperties, boolean preferExtensionDecoders, + public PlaylistSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo, UriSample... children) { - super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders); + super(name, preferExtensionDecoders, drmInfo); this.children = children; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index b4dab7b971..d6776c8ed0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -27,29 +27,36 @@ import android.os.Message; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; -import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; -import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; /** * A {@link DrmSession} that supports playbacks using {@link MediaDrm}. */ @TargetApi(18) /* package */ class DefaultDrmSession implements DrmSession { - private static final String TAG = "DefaultDrmSession"; - private static final String CENC_SCHEME_MIME_TYPE = "cenc"; + /** + * Listener of {@link DefaultDrmSession} events. + */ + public interface EventListener { + + /** + * Called each time provision is completed. + */ + void onProvisionCompleted(); + + } + + private static final String TAG = "DefaultDrmSession"; private static final int MSG_PROVISION = 0; private static final int MSG_KEYS = 1; - private static final int MAX_LICENSE_DURATION_TO_RENEW = 60; private final Handler eventHandler; @@ -58,7 +65,6 @@ import java.util.UUID; private final HashMap optionalKeyRequestParameters; /* package */ final MediaDrmCallback callback; /* package */ final UUID uuid; - /* package */ MediaDrmHandler mediaDrmHandler; /* package */ PostResponseHandler postResponseHandler; private HandlerThread requestHandlerThread; private Handler postRequestHandler; @@ -66,13 +72,14 @@ import java.util.UUID; @DefaultDrmSessionManager.Mode private final int mode; private int openCount; - private boolean provisioningInProgress; + private final AtomicBoolean provisioningInProgress; + private final EventListener sessionEventListener; @DrmSession.State private int state; private T mediaCrypto; private DrmSessionException lastException; - private final byte[] schemeInitData; - private final String schemeMimeType; + private final byte[] initData; + private final String mimeType; private byte[] sessionId; private byte[] offlineLicenseKeySetId; @@ -90,11 +97,12 @@ import java.util.UUID; * @param eventHandler The handler to post listener events. * @param eventListener The DRM session manager event listener. */ - public DefaultDrmSession(UUID uuid, ExoMediaDrm mediaDrm, DrmInitData initData, + public DefaultDrmSession(UUID uuid, ExoMediaDrm mediaDrm, byte[] initData, String mimeType, @DefaultDrmSessionManager.Mode int mode, byte[] offlineLicenseKeySetId, HashMap optionalKeyRequestParameters, MediaDrmCallback callback, Looper playbackLooper, Handler eventHandler, - DefaultDrmSessionManager.EventListener eventListener) { + DefaultDrmSessionManager.EventListener eventListener, AtomicBoolean provisioningInProgress, + EventListener sessionEventListener) { this.uuid = uuid; this.mediaDrm = mediaDrm; this.mode = mode; @@ -104,44 +112,22 @@ import java.util.UUID; this.eventHandler = eventHandler; this.eventListener = eventListener; + this.provisioningInProgress = provisioningInProgress; + this.sessionEventListener = sessionEventListener; state = STATE_OPENING; - mediaDrmHandler = new MediaDrmHandler(playbackLooper); - mediaDrm.setOnEventListener(new MediaDrmEventListener()); postResponseHandler = new PostResponseHandler(playbackLooper); requestHandlerThread = new HandlerThread("DrmRequestHandler"); requestHandlerThread.start(); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); - // Parse init data. - byte[] schemeInitData = null; - String schemeMimeType = null; if (offlineLicenseKeySetId == null) { - SchemeData data = getSchemeData(initData, uuid); - if (data == null) { - onError(new IllegalStateException("Media does not support uuid: " + uuid)); - } else { - schemeInitData = data.data; - schemeMimeType = data.mimeType; - if (Util.SDK_INT < 21) { - // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. - byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, uuid); - if (psshData == null) { - // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. - } else { - schemeInitData = psshData; - } - } - if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid) - && (MimeTypes.VIDEO_MP4.equals(schemeMimeType) - || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) { - // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4. - schemeMimeType = CENC_SCHEME_MIME_TYPE; - } - } + this.initData = initData; + this.mimeType = mimeType; + } else { + this.initData = null; + this.mimeType = null; } - this.schemeInitData = schemeInitData; - this.schemeMimeType = schemeMimeType; } // Life cycle. @@ -163,9 +149,6 @@ import java.util.UUID; public boolean release() { if (--openCount == 0) { state = STATE_RELEASED; - provisioningInProgress = false; - mediaDrmHandler.removeCallbacksAndMessages(null); - mediaDrmHandler = null; postResponseHandler.removeCallbacksAndMessages(null); postRequestHandler.removeCallbacksAndMessages(null); postRequestHandler = null; @@ -182,6 +165,14 @@ import java.util.UUID; return false; } + public boolean canReuse(byte[] initData) { + return Arrays.equals(this.initData, initData); + } + + public boolean hasSessionId(byte[] sessionId) { + return Arrays.equals(this.sessionId, sessionId); + } + // DrmSession Implementation. @Override @@ -245,21 +236,15 @@ import java.util.UUID; } private void postProvisionRequest() { - if (provisioningInProgress) { + if (provisioningInProgress.getAndSet(true)) { return; } - provisioningInProgress = true; ProvisionRequest request = mediaDrm.getProvisionRequest(); postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget(); } private void onProvisionResponse(Object response) { - provisioningInProgress = false; - if (state != STATE_OPENING && !isOpen()) { - // This event is stale. - return; - } - + provisioningInProgress.set(false); if (response instanceof Exception) { onError((Exception) response); return; @@ -267,11 +252,24 @@ import java.util.UUID; try { mediaDrm.provideProvisionResponse((byte[]) response); - if (openInternal(false)) { - doLicense(); - } } catch (DeniedByServerException e) { onError(e); + return; + } + + if (sessionEventListener != null) { + sessionEventListener.onProvisionCompleted(); + } + } + + public void onProvisionCompleted() { + if (state != STATE_OPENING && !isOpen()) { + // This event is stale. + return; + } + + if (openInternal(false)) { + doLicense(); } } @@ -322,6 +320,8 @@ import java.util.UUID; postKeyRequest(MediaDrm.KEY_TYPE_RELEASE); } break; + default: + break; } } @@ -347,7 +347,7 @@ import java.util.UUID; private void postKeyRequest(int type) { byte[] scope = type == MediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId; try { - KeyRequest request = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, type, + KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters); postRequestHandler.obtainMessage(MSG_KEYS, request).sendToTarget(); } catch (Exception e) { @@ -433,46 +433,27 @@ import java.util.UUID; return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS; } - @SuppressLint("HandlerLeak") - private class MediaDrmHandler extends Handler { - - public MediaDrmHandler(Looper looper) { - super(looper); + @SuppressWarnings("deprecation") + public void onMediaDrmEvent(int what) { + if (!isOpen()) { + return; } - - @SuppressWarnings("deprecation") - @Override - public void handleMessage(Message msg) { - if (!isOpen()) { - return; - } - switch (msg.what) { - case MediaDrm.EVENT_KEY_REQUIRED: - doLicense(); - break; - case MediaDrm.EVENT_KEY_EXPIRED: - // When an already expired key is loaded MediaDrm sends this event immediately. Ignore - // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still - // waiting for key response. - onKeysExpired(); - break; - case MediaDrm.EVENT_PROVISION_REQUIRED: - state = STATE_OPENED; - postProvisionRequest(); - break; - } - } - - } - - private class MediaDrmEventListener implements OnEventListener { - - @Override - public void onEvent(ExoMediaDrm md, byte[] sessionId, int event, int extra, - byte[] data) { - if (mode == DefaultDrmSessionManager.MODE_PLAYBACK) { - mediaDrmHandler.sendEmptyMessage(event); - } + switch (what) { + case MediaDrm.EVENT_KEY_REQUIRED: + doLicense(); + break; + case MediaDrm.EVENT_KEY_EXPIRED: + // When an already expired key is loaded MediaDrm sends this event immediately. Ignore + // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still + // waiting for key response. + onKeysExpired(); + break; + case MediaDrm.EVENT_PROVISION_REQUIRED: + state = STATE_OPENED; + postProvisionRequest(); + break; + default: + break; } } @@ -493,6 +474,9 @@ import java.util.UUID; case MSG_KEYS: onKeyResponse(msg.obj); break; + default: + break; + } } @@ -527,20 +511,4 @@ import java.util.UUID; } - /** - * Extracts {@link SchemeData} suitable for the given DRM scheme {@link UUID}. - * - * @param drmInitData The {@link DrmInitData} from which to extract the {@link SchemeData}. - * @param uuid The UUID of the scheme. - * @return The extracted {@link SchemeData}, or null if no suitable data is present. - */ - public static SchemeData getSchemeData(DrmInitData drmInitData, UUID uuid) { - SchemeData schemeData = drmInitData.get(uuid); - if (schemeData == null && C.CLEARKEY_UUID.equals(uuid)) { - // If present, the Common PSSH box should be used for ClearKey. - schemeData = drmInitData.get(C.COMMON_PSSH_UUID); - } - return schemeData; - } - } 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 9ea696e074..67931d106b 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 @@ -15,27 +15,36 @@ */ package com.google.android.exoplayer2.drm; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.media.MediaDrm; import android.os.Handler; import android.os.Looper; +import android.os.Message; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; +import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; /** * A {@link DrmSessionManager} that supports playbacks using {@link MediaDrm}. */ @TargetApi(18) -public class DefaultDrmSessionManager implements DrmSessionManager { +public class DefaultDrmSessionManager implements DrmSessionManager, + DefaultDrmSession.EventListener { /** * Listener of {@link DefaultDrmSessionManager} events. @@ -70,6 +79,7 @@ public class DefaultDrmSessionManager implements DrmSe * The key to use when passing CustomData to a PlayReady instance in an optional parameter map. */ public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData"; + private static final String CENC_SCHEME_MIME_TYPE = "cenc"; /** Determines the action to be done after a session acquired. */ @Retention(RetentionPolicy.SOURCE) @@ -93,14 +103,17 @@ public class DefaultDrmSessionManager implements DrmSe private final EventListener eventListener; private final ExoMediaDrm mediaDrm; private final HashMap optionalKeyRequestParameters; - private final MediaDrmCallback callback; private final UUID uuid; + private final boolean multiSession; private Looper playbackLooper; private int mode; private byte[] offlineLicenseKeySetId; - private DefaultDrmSession session; + + private final List> sessions; + private final AtomicBoolean provisioningInProgress; + /* package */ MediaDrmHandler mediaDrmHandler; /** * Instantiates a new instance using the Widevine scheme. @@ -163,7 +176,7 @@ public class DefaultDrmSessionManager implements DrmSe UUID uuid, MediaDrmCallback callback, HashMap optionalKeyRequestParameters, Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback, - optionalKeyRequestParameters, eventHandler, eventListener); + optionalKeyRequestParameters, eventHandler, eventListener, false); } /** @@ -179,7 +192,27 @@ public class DefaultDrmSessionManager implements DrmSe public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, HashMap optionalKeyRequestParameters, Handler eventHandler, EventListener eventListener) { + this(uuid, mediaDrm, callback, optionalKeyRequestParameters, eventHandler, eventListener, + false); + } + + /** + * @param uuid The UUID of the drm scheme. + * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. + * @param callback Performs key and provisioning requests. + * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument + * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param multiSession A boolean that specify whether multiple key session support is enabled. + * Default is false. + */ + public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, + HashMap optionalKeyRequestParameters, Handler eventHandler, + EventListener eventListener, boolean multiSession) { Assertions.checkNotNull(uuid); + Assertions.checkNotNull(mediaDrm); Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); this.uuid = uuid; this.mediaDrm = mediaDrm; @@ -187,7 +220,13 @@ public class DefaultDrmSessionManager implements DrmSe this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.eventHandler = eventHandler; this.eventListener = eventListener; + this.multiSession = multiSession; mode = MODE_PLAYBACK; + sessions = new ArrayList<>(); + provisioningInProgress = new AtomicBoolean(false); + if (multiSession) { + mediaDrm.setPropertyString("sessionSharing", "enable"); + } } /** @@ -261,7 +300,7 @@ public class DefaultDrmSessionManager implements DrmSe * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode. */ public void setMode(@Mode int mode, byte[] offlineLicenseKeySetId) { - Assertions.checkState(session == null); + Assertions.checkState(sessions.isEmpty()); if (mode == MODE_QUERY || mode == MODE_RELEASE) { Assertions.checkNotNull(offlineLicenseKeySetId); } @@ -273,7 +312,7 @@ public class DefaultDrmSessionManager implements DrmSe @Override public boolean canAcquireSession(@NonNull DrmInitData drmInitData) { - SchemeData schemeData = DefaultDrmSession.getSchemeData(drmInitData, uuid); + SchemeData schemeData = getSchemeData(drmInitData, uuid); if (schemeData == null) { // No data for this manager's scheme. return false; @@ -294,22 +333,144 @@ public class DefaultDrmSessionManager implements DrmSe @Override public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData) { Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper); - if (session == null) { + if (sessions.isEmpty()) { this.playbackLooper = playbackLooper; - session = new DefaultDrmSession(uuid, mediaDrm, drmInitData, mode, offlineLicenseKeySetId, - optionalKeyRequestParameters, callback, playbackLooper, eventHandler, eventListener); + mediaDrmHandler = new MediaDrmHandler(playbackLooper); + mediaDrm.setOnEventListener(new MediaDrmEventListener()); } + DefaultDrmSession session = null; + byte[] initData = null; + String mimeType = null; + + if (offlineLicenseKeySetId == null) { + SchemeData data = getSchemeData(drmInitData, uuid); + if (data == null) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrmSessionManagerError(new IllegalStateException( + "Media does not support uuid: " + uuid)); + } + }); + } + } else { + initData = getSchemeInitData(data, uuid); + mimeType = getSchemeMimeType(data, uuid); + } + } + + for (DefaultDrmSession s : sessions) { + if (!multiSession || s.canReuse(initData)) { + session = s; + break; + } + } + + if (session == null) { + session = new DefaultDrmSession(uuid, mediaDrm, initData, mimeType, mode, + offlineLicenseKeySetId, optionalKeyRequestParameters, callback, playbackLooper, + eventHandler, eventListener, provisioningInProgress, this); + sessions.add(session); + } session.acquire(); return session; } @Override public void releaseSession(DrmSession session) { - Assertions.checkState(session == this.session); - if (this.session.release()) { - this.session = null; + DefaultDrmSession drmSession = (DefaultDrmSession) session; + if (drmSession.release()) { + sessions.remove(drmSession); + } + + if (sessions.isEmpty()) { + mediaDrm.setOnEventListener(null); + mediaDrmHandler.removeCallbacksAndMessages(null); + mediaDrmHandler = null; + playbackLooper = null; } } + @Override + public void onProvisionCompleted() { + for (DefaultDrmSession session : sessions) { + session.onProvisionCompleted(); + } + } + + /** + * Extracts {@link SchemeData} suitable for the given DRM scheme {@link UUID}. + * + * @param drmInitData The {@link DrmInitData} from which to extract the {@link SchemeData}. + * @param uuid The UUID. + * @return The extracted {@link SchemeData}, or null if no suitable data is present. + */ + private static SchemeData getSchemeData(DrmInitData drmInitData, UUID uuid) { + SchemeData schemeData = drmInitData.get(uuid); + if (schemeData == null && C.CLEARKEY_UUID.equals(uuid)) { + // If present, the Common PSSH box should be used for ClearKey. + schemeData = drmInitData.get(C.COMMON_PSSH_UUID); + } + return schemeData; + } + + private static byte[] getSchemeInitData(SchemeData data, UUID uuid) { + byte[] schemeInitData = data.data; + if (Util.SDK_INT < 21) { + // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. + byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, uuid); + if (psshData == null) { + // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. + } else { + schemeInitData = psshData; + } + } + return schemeInitData; + } + + private static String getSchemeMimeType(SchemeData data, UUID uuid) { + String schemeMimeType = data.mimeType; + if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid) + && (MimeTypes.VIDEO_MP4.equals(schemeMimeType) + || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) { + // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4. + schemeMimeType = CENC_SCHEME_MIME_TYPE; + } + return schemeMimeType; + } + + @SuppressLint("HandlerLeak") + private class MediaDrmHandler extends Handler { + + public MediaDrmHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + byte[] sessionId = (byte[]) msg.obj; + for (DefaultDrmSession session : sessions) { + if (session.hasSessionId(sessionId)) { + session.onMediaDrmEvent(msg.what); + return; + } + } + } + + } + + private class MediaDrmEventListener implements OnEventListener { + + @Override + public void onEvent(ExoMediaDrm md, byte[] sessionId, int event, int extra, + byte[] data) { + if (mode == DefaultDrmSessionManager.MODE_PLAYBACK) { + mediaDrmHandler.obtainMessage(event, sessionId).sendToTarget(); + } + } + + } + } From fb023da529d9d86ec2f7c921dae7ba084fa50e5c Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Sep 2017 13:06:02 -0700 Subject: [PATCH 0061/1327] Fix attr inheritance in SimpleExoPlayerView When creating PlaybackControlView inside SimpleExoPlayerView, we want certain attributes to be passed through. This lets you set control attributes on the SimpleExoPlayerView directly. We don't want all attributes to be propagated though; only our own custom ones. Not sure if there's a cleaner way to do this. Pragmatically this solution seems ... ok :)? ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167619801 --- .../android/exoplayer2/ui/PlaybackControlView.java | 10 +++++++--- .../android/exoplayer2/ui/SimpleExoPlayerView.java | 8 ++++---- 2 files changed, 11 insertions(+), 7 deletions(-) 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 c89feaebf5..9bbb2fa27b 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 @@ -298,16 +298,20 @@ public class PlaybackControlView extends FrameLayout { } public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); + this(context, attrs, defStyleAttr, attrs); + } + public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr, + AttributeSet playbackAttrs) { + super(context, attrs, defStyleAttr); int controllerLayoutId = R.layout.exo_playback_control_view; rewindMs = DEFAULT_REWIND_MS; fastForwardMs = DEFAULT_FAST_FORWARD_MS; showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; showShuffleButton = false; - if (attrs != null) { - TypedArray a = context.getTheme().obtainStyledAttributes(attrs, + if (playbackAttrs != null) { + TypedArray a = context.getTheme().obtainStyledAttributes(playbackAttrs, R.styleable.PlaybackControlView, 0, 0); try { rewindMs = a.getInt(R.styleable.PlaybackControlView_rewind_increment, rewindMs); 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 5b6e11c5e4..488411550c 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 @@ -240,7 +240,7 @@ public final class SimpleExoPlayerView extends FrameLayout { controller = null; componentListener = null; overlayFrameLayout = null; - ImageView logo = new ImageView(context, attrs); + ImageView logo = new ImageView(context); if (Util.SDK_INT >= 23) { configureEditModeLogoV23(getResources(), logo); } else { @@ -330,9 +330,9 @@ public final class SimpleExoPlayerView extends FrameLayout { if (customController != null) { this.controller = customController; } else if (controllerPlaceholder != null) { - // Note: rewindMs and fastForwardMs are passed via attrs, so we don't need to make explicit - // calls to set them. - this.controller = new PlaybackControlView(context, attrs); + // Propagate attrs as playbackAttrs so that PlaybackControlView's custom attributes are + // transferred, but standard FrameLayout attributes (e.g. background) are not. + this.controller = new PlaybackControlView(context, null, 0, attrs); controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); int controllerIndex = parent.indexOfChild(controllerPlaceholder); From 8ef6a2e7bd4f10c4e1600b67bfc94a1ab00865b7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 6 Sep 2017 00:14:55 -0700 Subject: [PATCH 0062/1327] Clear gapless playback metadata for clipped media Also pass an unresolved end point to ClippingMediaPeriod. This removes some assertions checking timestamps in the ClippingMediaPeriod, but makes it possible to identify when the end point is at the end of the media. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167683358 --- .../android/exoplayer2/source/ClippingMediaPeriod.java | 9 ++++++++- .../android/exoplayer2/source/ClippingMediaSource.java | 5 +---- 2 files changed, 9 insertions(+), 5 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 a8c33b4625..89af07a3f0 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 @@ -270,7 +270,14 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb return C.RESULT_BUFFER_READ; } int result = stream.readData(formatHolder, buffer, requireFormat); - // TODO: Clear gapless playback metadata if a format was read (if applicable). + if (result == C.RESULT_FORMAT_READ) { + // Clear gapless playback metadata if the start/end points don't match the media. + Format format = formatHolder.format; + int encoderDelay = startUs != 0 ? 0 : format.encoderDelay; + int encoderPadding = endUs != C.TIME_END_OF_SOURCE ? 0 : format.encoderPadding; + formatHolder.format = format.copyWithGaplessInfo(encoderDelay, encoderPadding); + return C.RESULT_FORMAT_READ; + } if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ && mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) { 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 2387b43d5e..21de83524a 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 @@ -98,7 +98,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod( mediaSource.createPeriod(id, allocator), enableInitialDiscontinuity); mediaPeriods.add(mediaPeriod); - mediaPeriod.setClipping(clippingTimeline.startUs, clippingTimeline.endUs); + mediaPeriod.setClipping(startUs, endUs); return mediaPeriod; } @@ -119,9 +119,6 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { clippingTimeline = new ClippingTimeline(timeline, startUs, endUs); sourceListener.onSourceInfoRefreshed(clippingTimeline, manifest); - long startUs = clippingTimeline.startUs; - long endUs = clippingTimeline.endUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE - : clippingTimeline.endUs; int count = mediaPeriods.size(); for (int i = 0; i < count; i++) { mediaPeriods.get(i).setClipping(startUs, endUs); From 17232f58a339809279c0d7f8432a8014ce72309b Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Sep 2017 03:51:04 -0700 Subject: [PATCH 0063/1327] Fix position reporting during ads when period has non-zero window offset. Reporting incorrect positions for ad playbacks was causing IMA to think the ad wasn't playing, when in fact it was. Issue: #3180 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167702032 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 4 +++- .../android/exoplayer2/ExoPlayerImpl.java | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 6 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 c27ef17b87..87033173de 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 @@ -430,7 +430,9 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, } else if (!playingAd) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } else { - return new VideoProgressUpdate(player.getCurrentPosition(), player.getDuration()); + long adDuration = player.getDuration(); + return adDuration == C.TIME_UNSET ? VideoProgressUpdate.VIDEO_TIME_NOT_READY + : new VideoProgressUpdate(player.getCurrentPosition(), adDuration); } } 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 7bf0cd5a02..e574bfc1ee 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 @@ -349,8 +349,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingWindowPositionMs; } else { - timeline.getPeriod(playbackInfo.periodId.periodIndex, period); - return period.getPositionInWindowMs() + C.usToMs(playbackInfo.positionUs); + return playbackInfoPositionUsToWindowPositionMs(playbackInfo.positionUs); } } @@ -360,8 +359,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingWindowPositionMs; } else { - timeline.getPeriod(playbackInfo.periodId.periodIndex, period); - return period.getPositionInWindowMs() + C.usToMs(playbackInfo.bufferedPositionUs); + return playbackInfoPositionUsToWindowPositionMs(playbackInfo.bufferedPositionUs); } } @@ -388,7 +386,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public boolean isPlayingAd() { - return pendingSeekAcks == 0 && playbackInfo.periodId.adGroupIndex != C.INDEX_UNSET; + return pendingSeekAcks == 0 && playbackInfo.periodId.isAd(); } @Override @@ -542,4 +540,13 @@ import java.util.concurrent.CopyOnWriteArraySet; } } + private long playbackInfoPositionUsToWindowPositionMs(long positionUs) { + long positionMs = C.usToMs(positionUs); + if (!playbackInfo.periodId.isAd()) { + timeline.getPeriod(playbackInfo.periodId.periodIndex, period); + positionMs += period.getPositionInWindowMs(); + } + return positionMs; + } + } From 013379fd3ee299ab6f24e8d42686af13249d54c2 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Sep 2017 05:40:33 -0700 Subject: [PATCH 0064/1327] Workaround for SurfaceView not being hidden properly This appears to be fixed in Oreo, but given how harmless the workaround is we can probably just apply it on all API levels to be sure. Issue: #3160 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167709070 --- .../android/exoplayer2/ui/SimpleExoPlayerView.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 488411550c..8fa264cad1 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 @@ -428,6 +428,15 @@ public final class SimpleExoPlayerView extends FrameLayout { } } + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + if (surfaceView instanceof SurfaceView) { + // Work around https://github.com/google/ExoPlayer/issues/3160 + surfaceView.setVisibility(visibility); + } + } + /** * Sets the resize mode. * From e7992513d3494c273cba15e97a9292527f46f668 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Sep 2017 05:58:56 -0700 Subject: [PATCH 0065/1327] Remove references to MediaDrm from DefaultDrmSession classes Everything should go through the ExoMediaDrm layer. We still need to abstract away the android.media exception classes, but this is left as future work. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167710213 --- .../exoplayer2/drm/DefaultDrmSession.java | 23 +++++------ .../drm/DefaultDrmSessionManager.java | 19 +++++---- .../android/exoplayer2/drm/ExoMediaDrm.java | 39 ++++++++++++++++++- .../exoplayer2/drm/FrameworkMediaDrm.java | 4 +- 4 files changed, 58 insertions(+), 27 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index d6776c8ed0..d9550c756c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.drm; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.media.DeniedByServerException; -import android.media.MediaDrm; import android.media.NotProvisionedException; import android.os.Handler; import android.os.HandlerThread; @@ -36,7 +35,7 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; /** - * A {@link DrmSession} that supports playbacks using {@link MediaDrm}. + * A {@link DrmSession} that supports playbacks using {@link ExoMediaDrm}. */ @TargetApi(18) /* package */ class DefaultDrmSession implements DrmSession { @@ -227,8 +226,6 @@ import java.util.concurrent.atomic.AtomicBoolean; onError(e); } } catch (Exception e) { - // MediaCryptoException - // ResourceBusyException only available on 19+ onError(e); } @@ -278,7 +275,7 @@ import java.util.concurrent.atomic.AtomicBoolean; case DefaultDrmSessionManager.MODE_PLAYBACK: case DefaultDrmSessionManager.MODE_QUERY: if (offlineLicenseKeySetId == null) { - postKeyRequest(MediaDrm.KEY_TYPE_STREAMING); + postKeyRequest(ExoMediaDrm.KEY_TYPE_STREAMING); } else { if (restoreKeys()) { long licenseDurationRemainingSec = getLicenseDurationRemainingSec(); @@ -286,7 +283,7 @@ import java.util.concurrent.atomic.AtomicBoolean; && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) { Log.d(TAG, "Offline license has expired or will expire soon. " + "Remaining seconds: " + licenseDurationRemainingSec); - postKeyRequest(MediaDrm.KEY_TYPE_OFFLINE); + postKeyRequest(ExoMediaDrm.KEY_TYPE_OFFLINE); } else if (licenseDurationRemainingSec <= 0) { onError(new KeysExpiredException()); } else { @@ -305,11 +302,11 @@ import java.util.concurrent.atomic.AtomicBoolean; break; case DefaultDrmSessionManager.MODE_DOWNLOAD: if (offlineLicenseKeySetId == null) { - postKeyRequest(MediaDrm.KEY_TYPE_OFFLINE); + postKeyRequest(ExoMediaDrm.KEY_TYPE_OFFLINE); } else { // Renew if (restoreKeys()) { - postKeyRequest(MediaDrm.KEY_TYPE_OFFLINE); + postKeyRequest(ExoMediaDrm.KEY_TYPE_OFFLINE); } } break; @@ -317,7 +314,7 @@ import java.util.concurrent.atomic.AtomicBoolean; // 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(MediaDrm.KEY_TYPE_RELEASE); + postKeyRequest(ExoMediaDrm.KEY_TYPE_RELEASE); } break; default: @@ -345,7 +342,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void postKeyRequest(int type) { - byte[] scope = type == MediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId; + byte[] scope = type == ExoMediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId; try { KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters); @@ -439,16 +436,16 @@ import java.util.concurrent.atomic.AtomicBoolean; return; } switch (what) { - case MediaDrm.EVENT_KEY_REQUIRED: + case ExoMediaDrm.EVENT_KEY_REQUIRED: doLicense(); break; - case MediaDrm.EVENT_KEY_EXPIRED: + case ExoMediaDrm.EVENT_KEY_EXPIRED: // When an already expired key is loaded MediaDrm sends this event immediately. Ignore // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still // waiting for key response. onKeysExpired(); break; - case MediaDrm.EVENT_PROVISION_REQUIRED: + case ExoMediaDrm.EVENT_PROVISION_REQUIRED: state = STATE_OPENED; postProvisionRequest(); break; 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 67931d106b..029b41fde8 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 @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.drm; import android.annotation.SuppressLint; import android.annotation.TargetApi; -import android.media.MediaDrm; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -40,7 +39,7 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; /** - * A {@link DrmSessionManager} that supports playbacks using {@link MediaDrm}. + * A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */ @TargetApi(18) public class DefaultDrmSessionManager implements DrmSessionManager, @@ -120,7 +119,7 @@ public class DefaultDrmSessionManager implements DrmSe * * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + * to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. @@ -166,7 +165,7 @@ public class DefaultDrmSessionManager implements DrmSe * @param uuid The UUID of the drm scheme. * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + * to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. @@ -184,7 +183,7 @@ public class DefaultDrmSessionManager implements DrmSe * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + * to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. @@ -201,7 +200,7 @@ public class DefaultDrmSessionManager implements DrmSe * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + * to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. @@ -230,7 +229,7 @@ public class DefaultDrmSessionManager implements DrmSe } /** - * Provides access to {@link MediaDrm#getPropertyString(String)}. + * Provides access to {@link ExoMediaDrm#getPropertyString(String)}. *

    * This method may be called when the manager is in any state. * @@ -242,7 +241,7 @@ public class DefaultDrmSessionManager implements DrmSe } /** - * Provides access to {@link MediaDrm#setPropertyString(String, String)}. + * Provides access to {@link ExoMediaDrm#setPropertyString(String, String)}. *

    * This method may be called when the manager is in any state. * @@ -254,7 +253,7 @@ public class DefaultDrmSessionManager implements DrmSe } /** - * Provides access to {@link MediaDrm#getPropertyByteArray(String)}. + * Provides access to {@link ExoMediaDrm#getPropertyByteArray(String)}. *

    * This method may be called when the manager is in any state. * @@ -266,7 +265,7 @@ public class DefaultDrmSessionManager implements DrmSe } /** - * Provides access to {@link MediaDrm#setPropertyByteArray(String, byte[])}. + * Provides access to {@link ExoMediaDrm#setPropertyByteArray(String, byte[])}. *

    * This method may be called when the manager is in any state. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java index 63387f19e1..25b065c543 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java @@ -15,19 +15,54 @@ */ package com.google.android.exoplayer2.drm; +import android.annotation.TargetApi; import android.media.DeniedByServerException; import android.media.MediaCryptoException; import android.media.MediaDrm; +import android.media.MediaDrmException; import android.media.NotProvisionedException; -import android.media.ResourceBusyException; import java.util.HashMap; import java.util.Map; +import java.util.UUID; /** * Used to obtain keys for decrypting protected media streams. See {@link android.media.MediaDrm}. */ +@TargetApi(18) public interface ExoMediaDrm { + /** + * @see MediaDrm#EVENT_KEY_REQUIRED + */ + @SuppressWarnings("InlinedApi") + int EVENT_KEY_REQUIRED = MediaDrm.EVENT_KEY_REQUIRED; + /** + * @see MediaDrm#EVENT_KEY_EXPIRED + */ + @SuppressWarnings("InlinedApi") + int EVENT_KEY_EXPIRED = MediaDrm.EVENT_KEY_EXPIRED; + /** + * @see MediaDrm#EVENT_PROVISION_REQUIRED + */ + @SuppressWarnings("InlinedApi") + int EVENT_PROVISION_REQUIRED = MediaDrm.EVENT_PROVISION_REQUIRED; + + /** + * @see MediaDrm#KEY_TYPE_STREAMING + */ + @SuppressWarnings("InlinedApi") + int KEY_TYPE_STREAMING = MediaDrm.KEY_TYPE_STREAMING; + /** + * @see MediaDrm#KEY_TYPE_OFFLINE + */ + @SuppressWarnings("InlinedApi") + int KEY_TYPE_OFFLINE = MediaDrm.KEY_TYPE_OFFLINE; + /** + * @see MediaDrm#KEY_TYPE_RELEASE + */ + @SuppressWarnings("InlinedApi") + int KEY_TYPE_RELEASE = MediaDrm.KEY_TYPE_RELEASE; + /** * @see android.media.MediaDrm.OnEventListener */ @@ -69,7 +104,7 @@ public interface ExoMediaDrm { /** * @see MediaDrm#openSession() */ - byte[] openSession() throws NotProvisionedException, ResourceBusyException; + byte[] openSession() throws MediaDrmException; /** * @see MediaDrm#closeSession(byte[]) 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 d664cb69a9..2edd6ff254 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 @@ -20,8 +20,8 @@ import android.media.DeniedByServerException; import android.media.MediaCrypto; import android.media.MediaCryptoException; import android.media.MediaDrm; +import android.media.MediaDrmException; import android.media.NotProvisionedException; -import android.media.ResourceBusyException; import android.media.UnsupportedSchemeException; import android.support.annotation.NonNull; import com.google.android.exoplayer2.C; @@ -79,7 +79,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm Date: Wed, 6 Sep 2017 06:10:02 -0700 Subject: [PATCH 0066/1327] Add full stops ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167711267 --- .../android/exoplayer2/extractor/mp4/AtomParsers.java | 2 +- .../android/exoplayer2/mediacodec/MediaCodecUtil.java | 6 +++--- .../android/exoplayer2/metadata/MetadataRenderer.java | 4 ++-- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 2 +- .../google/android/exoplayer2/ui/SimpleExoPlayerView.java | 2 +- 5 files changed, 8 insertions(+), 8 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 450e0682e6..c754c4b566 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 @@ -1147,7 +1147,7 @@ import java.util.List; } /** - * Parses the proj box from sv3d box, as specified by https://github.com/google/spatial-media + * Parses the proj box from sv3d box, as specified by https://github.com/google/spatial-media. */ private static byte[] parseProjFromParent(ParsableByteArray parent, int position, int size) { int childPosition = position + Atom.HEADER_SIZE; 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 1073e8d9c1..2cd336e9ef 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 @@ -283,13 +283,13 @@ public final class MediaCodecUtil { return false; } - // Work around https://github.com/google/ExoPlayer/issues/398 + // Work around https://github.com/google/ExoPlayer/issues/398. if (Util.SDK_INT < 18 && "OMX.SEC.MP3.Decoder".equals(name)) { return false; } // Work around https://github.com/google/ExoPlayer/issues/1528 and - // https://github.com/google/ExoPlayer/issues/3171 + // https://github.com/google/ExoPlayer/issues/3171. if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name) && ("a70".equals(Util.DEVICE) || ("Xiaomi".equals(Util.MANUFACTURER) && Util.DEVICE.startsWith("HM")))) { @@ -325,7 +325,7 @@ public final class MediaCodecUtil { return false; } - // Work around https://github.com/google/ExoPlayer/issues/548 + // Work around https://github.com/google/ExoPlayer/issues/548. // VP8 decoder on Samsung Galaxy S3/S4/S4 Mini/Tab 3/Note 2 does not render video. if (Util.SDK_INT <= 19 && "OMX.SEC.vp8.dec".equals(name) && "samsung".equals(Util.MANUFACTURER) 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 869e9306a5..7d36d87a9e 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 @@ -40,8 +40,8 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { private static final int MSG_INVOKE_RENDERER = 0; // TODO: Holding multiple pending metadata objects is temporary mitigation against - // https://github.com/google/ExoPlayer/issues/1874 - // It should be removed once this issue has been addressed. + // https://github.com/google/ExoPlayer/issues/1874. It should be removed once this issue has been + // addressed. private static final int MAX_PENDING_METADATA_COUNT = 5; private final MetadataDecoderFactory decoderFactory; 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 d72619dbe1..4c1f4c0eb2 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 @@ -979,7 +979,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * If true is returned then we fall back to releasing and re-instantiating the codec instead. */ private static boolean codecNeedsSetOutputSurfaceWorkaround(String name) { - // Work around https://github.com/google/ExoPlayer/issues/3236 + // Work around https://github.com/google/ExoPlayer/issues/3236. return ("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) && "OMX.qcom.video.decoder.avc".equals(name); } 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 8fa264cad1..9bc4bb5b87 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 @@ -432,7 +432,7 @@ public final class SimpleExoPlayerView extends FrameLayout { public void setVisibility(int visibility) { super.setVisibility(visibility); if (surfaceView instanceof SurfaceView) { - // Work around https://github.com/google/ExoPlayer/issues/3160 + // Work around https://github.com/google/ExoPlayer/issues/3160. surfaceView.setVisibility(visibility); } } From c6fa034eba88fe1a7fc62aae3288006ff4f3ad6d Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Sep 2017 06:17:09 -0700 Subject: [PATCH 0067/1327] DecryptionException cleanup + add missing header ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167711928 --- .../exoplayer2/drm/DecryptionException.java | 33 ++++++++++++++----- .../android/exoplayer2/drm/DrmSession.java | 4 ++- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java index 6916b972b2..81cfc26393 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java @@ -1,20 +1,37 @@ +/* + * 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.drm; /** - * An exception when doing drm decryption using the In-App Drm + * Thrown when a non-platform component fails to decrypt data. */ public class DecryptionException extends Exception { - private final int errorCode; + /** + * A component specific error code. + */ + public final int errorCode; + + /** + * @param errorCode A component specific error code. + * @param message The detail message. + */ public DecryptionException(int errorCode, String message) { super(message); this.errorCode = errorCode; } - /** - * Get error code - */ - public int getErrorCode() { - return errorCode; - } } 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 0c17b102fd..a3ae1d8b71 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,7 +28,9 @@ import java.util.Map; @TargetApi(16) public interface DrmSession { - /** Wraps the throwable 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(Throwable cause) { From 7d4190f3c84a223d189d5f0689778b1b047da886 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Sep 2017 07:22:00 -0700 Subject: [PATCH 0068/1327] Regroup final/non-final vars ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167717715 --- .../exoplayer2/drm/DefaultDrmSession.java | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index d9550c756c..4f187264a6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -58,30 +58,30 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_KEYS = 1; private static final int MAX_LICENSE_DURATION_TO_RENEW = 60; - private final Handler eventHandler; - private final DefaultDrmSessionManager.EventListener eventListener; private final ExoMediaDrm mediaDrm; - private final HashMap optionalKeyRequestParameters; - /* package */ final MediaDrmCallback callback; - /* package */ final UUID uuid; - /* package */ PostResponseHandler postResponseHandler; - private HandlerThread requestHandlerThread; - private Handler postRequestHandler; - - @DefaultDrmSessionManager.Mode - private final int mode; - private int openCount; - private final AtomicBoolean provisioningInProgress; - private final EventListener sessionEventListener; - @DrmSession.State - private int state; - private T mediaCrypto; - private DrmSessionException lastException; private final byte[] initData; private final String mimeType; + private final @DefaultDrmSessionManager.Mode int mode; + private final HashMap optionalKeyRequestParameters; + private final Handler eventHandler; + private final DefaultDrmSessionManager.EventListener eventListener; + private final AtomicBoolean provisioningInProgress; + private final EventListener sessionEventListener; + + /* package */ final MediaDrmCallback callback; + /* package */ final UUID uuid; + + private @DrmSession.State int state; + private int openCount; + private HandlerThread requestHandlerThread; + private Handler postRequestHandler; + private T mediaCrypto; + private DrmSessionException lastException; private byte[] sessionId; private byte[] offlineLicenseKeySetId; + /* package */ PostResponseHandler postResponseHandler; + /** * Instantiates a new DRM session. * From 4869a2506960899a94215df1fbf536e6542ac984 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Sep 2017 07:26:23 -0700 Subject: [PATCH 0069/1327] Pick up rtmpClient fix Issue: #3156 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167718081 --- extensions/rtmp/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index c832cb82e9..7687f03e32 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -26,7 +26,7 @@ android { dependencies { compile project(modulePrefix + 'library-core') - compile 'net.butterflytv.utils:rtmp-client:0.2.8' + compile 'net.butterflytv.utils:rtmp-client:3.0.0' } ext { From 7c3fe19d3f5e0a08d855d4564c5c09d9e41d2ba2 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Sep 2017 01:45:56 -0700 Subject: [PATCH 0070/1327] Migrate remaining tests to Robolectric Remaining instrumentation tests either use android.os.Handler or rely on assets. In the latter case, the tests are difficult to migrate due to differences between the internal and external build systems, and configuration needed in Android Studio. In addition, SimpleCacheSpanTest remains as an instrumentation test because it fails due to a problem with string encoding on the internal build (and two other tests in its package are kept with it because they depend on it). This test removes a dependency from testutils on Mockito, as a different version of Mockito needs to be used for instrumentation tests vs Robolectric tests, yet both sets of tests need to rely on testutils. Mockito setup is now done directly in the tests that need it. Move OggTestData to testutils so it can be used from both instrumentation and Robolectric tests. It may be possible to simplify assertions further using Truth but this is left for possible later changes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167831435 --- .../drm/OfflineLicenseHelperTest.java | 14 +- .../extractor/ogg/OggExtractorTest.java | 13 +- .../extractor/ogg/OggPacketTest.java | 43 +++-- .../exoplayer2/extractor/ogg/OggTestFile.java | 3 +- .../extractor/ogg/VorbisBitArrayTest.java | 139 -------------- .../extractor/ogg/VorbisReaderTest.java | 104 ---------- .../extractor/ogg/VorbisUtilTest.java | 126 ------------ .../exoplayer2/text/ttml/TtmlStyleTest.java | 132 ------------- .../cache/CachedRegionTrackerTest.java | 15 +- .../exoplayer2/drm/DrmInitDataTest.java | 82 ++++---- .../extractor/DefaultExtractorInputTest.java | 140 ++++++++------ .../exoplayer2/extractor/ExtractorTest.java | 20 +- .../extractor/mkv/DefaultEbmlReaderTest.java | 31 ++- .../extractor/mkv/VarintReaderTest.java | 30 ++- .../extractor/mp3/XingSeekerTest.java | 53 ++++-- .../extractor/mp4/AtomParsersTest.java | 20 +- .../extractor/mp4/PsshAtomUtilTest.java | 31 ++- .../ogg/DefaultOggSeekerUtilMethodsTest.java | 78 +++++--- .../extractor/ogg/OggPageHeaderTest.java | 54 ++++-- .../extractor/ogg/VorbisBitArrayTest.java | 155 +++++++++++++++ .../extractor/ogg/VorbisReaderTest.java | 117 ++++++++++++ .../extractor/ogg/VorbisUtilTest.java | 146 ++++++++++++++ .../extractor/ts/SectionReaderTest.java | 53 ++++-- .../emsg/EventMessageDecoderTest.java | 25 ++- .../metadata/emsg/EventMessageTest.java | 14 +- .../metadata/id3/ChapterFrameTest.java | 14 +- .../metadata/id3/ChapterTocFrameTest.java | 14 +- .../metadata/id3/Id3DecoderTest.java | 135 +++++++------ .../scte35/SpliceInfoDecoderTest.java | 78 ++++---- .../ProgressiveDownloadActionTest.java | 143 ++++++++++++++ .../exoplayer2/source/SampleQueueTest.java | 146 ++++++++------ .../exoplayer2/source/ShuffleOrderTest.java | 43 +++-- .../text/ttml/TtmlRenderUtilTest.java | 62 ++++-- .../exoplayer2/text/ttml/TtmlStyleTest.java | 155 +++++++++++++++ .../exoplayer2/text/webvtt/CssParserTest.java | 115 ++++++----- .../text/webvtt/Mp4WebvttDecoderTest.java | 38 ++-- .../text/webvtt/WebvttCueParserTest.java | 180 ++++++++++-------- .../text/webvtt/WebvttSubtitleTest.java | 76 +++++--- .../MappingTrackSelectorTest.java | 38 ++-- .../upstream/ByteArrayDataSourceTest.java | 42 ++-- .../upstream/DataSchemeDataSourceTest.java | 46 +++-- .../upstream/DataSourceAsserts.java | 50 +++++ .../upstream/DataSourceInputStreamTest.java | 51 +++-- .../upstream/cache/CacheAsserts.java | 115 +++++++++++ .../upstream/cache/CacheDataSourceTest.java | 74 ++++--- .../upstream/cache/CacheDataSourceTest2.java | 37 ++-- .../upstream/cache/CacheUtilTest.java | 68 +++++-- .../upstream/cache/SimpleCacheTest.java | 105 +++++----- .../crypto/AesFlushingCipherTest.java | 63 +++--- .../exoplayer2/source/dash/MockitoUtil.java | 38 ++++ .../dash/offline/DashDownloaderTest.java | 3 +- .../exoplayer2/testutil/OggTestData.java | 10 +- .../android/exoplayer2/testutil/TestUtil.java | 9 - 53 files changed, 2266 insertions(+), 1320 deletions(-) delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java (61%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java (78%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/ExtractorTest.java (60%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java (92%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java (92%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java (67%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java (79%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java (56%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java (72%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java (59%) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java (81%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java (70%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java (74%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java (75%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java (76%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java (66%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java (73%) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/source/SampleQueueTest.java (86%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java (78%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java (59%) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java (61%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java (82%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java (50%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java (78%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java (87%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java (82%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java (61%) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceAsserts.java rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java (65%) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java (77%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java (86%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java (83%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java (69%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java (77%) create mode 100644 library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java rename library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/TestData.java => testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java (99%) 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 821656475d..35bfbe613a 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 @@ -23,10 +23,10 @@ import android.test.MoreAsserts; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; -import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.HttpDataSource; import java.util.HashMap; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; /** * Tests {@link OfflineLicenseHelper}. @@ -40,7 +40,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - TestUtil.setUpMockito(this); + setUpMockito(this); when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3}); offlineLicenseHelper = new OfflineLicenseHelper<>(mediaDrm, mediaDrmCallback, null); } @@ -157,4 +157,14 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { new byte[] {1, 4, 7, 0, 3, 6})); } + /** + * Sets up Mockito for an instrumentation test. + */ + private static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { + // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. + System.setProperty("dexmaker.dexcache", + instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath()); + MockitoAnnotations.initMocks(instrumentationTestCase); + } + } 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 3be23422cc..b1ebdf3261 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 @@ -20,6 +20,7 @@ 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.OggTestData; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; @@ -56,7 +57,7 @@ public final class OggExtractorTest extends InstrumentationTestCase { public void testSniffVorbis() throws Exception { byte[] data = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x02, 0, 1000, 1), + OggTestData.buildOggHeader(0x02, 0, 1000, 1), TestUtil.createByteArray(7), // Laces new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); assertTrue(sniff(data)); @@ -64,7 +65,7 @@ public final class OggExtractorTest extends InstrumentationTestCase { public void testSniffFlac() throws Exception { byte[] data = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x02, 0, 1000, 1), + OggTestData.buildOggHeader(0x02, 0, 1000, 1), TestUtil.createByteArray(5), // Laces new byte[] {0x7F, 'F', 'L', 'A', 'C'}); assertTrue(sniff(data)); @@ -72,26 +73,26 @@ public final class OggExtractorTest extends InstrumentationTestCase { public void testSniffFailsOpusFile() throws Exception { byte[] data = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x02, 0, 1000, 0x00), + OggTestData.buildOggHeader(0x02, 0, 1000, 0x00), new byte[] {'O', 'p', 'u', 's'}); assertFalse(sniff(data)); } public void testSniffFailsInvalidOggHeader() throws Exception { - byte[] data = TestData.buildOggHeader(0x00, 0, 1000, 0x00); + byte[] data = OggTestData.buildOggHeader(0x00, 0, 1000, 0x00); assertFalse(sniff(data)); } public void testSniffInvalidHeader() throws Exception { byte[] data = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x02, 0, 1000, 1), + OggTestData.buildOggHeader(0x02, 0, 1000, 1), TestUtil.createByteArray(7), // Laces new byte[] {0x7F, 'X', 'o', 'r', 'b', 'i', 's'}); assertFalse(sniff(data)); } public void testSniffFailsEOF() throws Exception { - byte[] data = TestData.buildOggHeader(0x02, 0, 1000, 0x00); + byte[] data = OggTestData.buildOggHeader(0x02, 0, 1000, 0x00); assertFalse(sniff(data)); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java index 991d31ff03..186b842bab 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ogg; import android.test.InstrumentationTestCase; import android.test.MoreAsserts; import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.OggTestData; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; @@ -47,20 +48,20 @@ public final class OggPacketTest extends InstrumentationTestCase { byte[] thirdPacket = TestUtil.buildTestData(256, random); byte[] fourthPacket = TestUtil.buildTestData(271, random); - FakeExtractorInput input = TestData.createInput( + FakeExtractorInput input = OggTestData.createInput( TestUtil.joinByteArrays( // First page with a single packet. - TestData.buildOggHeader(0x02, 0, 1000, 0x01), + OggTestData.buildOggHeader(0x02, 0, 1000, 0x01), TestUtil.createByteArray(0x08), // Laces firstPacket, // Second page with a single packet. - TestData.buildOggHeader(0x00, 16, 1001, 0x02), + OggTestData.buildOggHeader(0x00, 16, 1001, 0x02), TestUtil.createByteArray(0xFF, 0x11), // Laces secondPacket, // Third page with zero packets. - TestData.buildOggHeader(0x00, 16, 1002, 0x00), + OggTestData.buildOggHeader(0x00, 16, 1002, 0x00), // Fourth page with two packets. - TestData.buildOggHeader(0x04, 128, 1003, 0x04), + OggTestData.buildOggHeader(0x04, 128, 1003, 0x04), TestUtil.createByteArray(0xFF, 0x01, 0xFF, 0x10), // Laces thirdPacket, fourthPacket), true); @@ -107,9 +108,9 @@ public final class OggPacketTest extends InstrumentationTestCase { byte[] firstPacket = TestUtil.buildTestData(255, random); byte[] secondPacket = TestUtil.buildTestData(8, random); - FakeExtractorInput input = TestData.createInput( + FakeExtractorInput input = OggTestData.createInput( TestUtil.joinByteArrays( - TestData.buildOggHeader(0x06, 0, 1000, 0x04), + OggTestData.buildOggHeader(0x06, 0, 1000, 0x04), TestUtil.createByteArray(0xFF, 0x00, 0x00, 0x08), // Laces. firstPacket, secondPacket), true); @@ -122,14 +123,14 @@ public final class OggPacketTest extends InstrumentationTestCase { public void testReadContinuedPacketOverTwoPages() throws Exception { byte[] firstPacket = TestUtil.buildTestData(518); - FakeExtractorInput input = TestData.createInput( + FakeExtractorInput input = OggTestData.createInput( TestUtil.joinByteArrays( // First page. - TestData.buildOggHeader(0x02, 0, 1000, 0x02), + OggTestData.buildOggHeader(0x02, 0, 1000, 0x02), TestUtil.createByteArray(0xFF, 0xFF), // Laces. Arrays.copyOf(firstPacket, 510), // Second page (continued packet). - TestData.buildOggHeader(0x05, 10, 1001, 0x01), + OggTestData.buildOggHeader(0x05, 10, 1001, 0x01), TestUtil.createByteArray(0x08), // Laces. Arrays.copyOfRange(firstPacket, 510, 510 + 8)), true); @@ -144,22 +145,22 @@ public final class OggPacketTest extends InstrumentationTestCase { public void testReadContinuedPacketOverFourPages() throws Exception { byte[] firstPacket = TestUtil.buildTestData(1028); - FakeExtractorInput input = TestData.createInput( + FakeExtractorInput input = OggTestData.createInput( TestUtil.joinByteArrays( // First page. - TestData.buildOggHeader(0x02, 0, 1000, 0x02), + OggTestData.buildOggHeader(0x02, 0, 1000, 0x02), TestUtil.createByteArray(0xFF, 0xFF), // Laces. Arrays.copyOf(firstPacket, 510), // Second page (continued packet). - TestData.buildOggHeader(0x01, 10, 1001, 0x01), + OggTestData.buildOggHeader(0x01, 10, 1001, 0x01), TestUtil.createByteArray(0xFF), // Laces. Arrays.copyOfRange(firstPacket, 510, 510 + 255), // Third page (continued packet). - TestData.buildOggHeader(0x01, 10, 1002, 0x01), + OggTestData.buildOggHeader(0x01, 10, 1002, 0x01), TestUtil.createByteArray(0xFF), // Laces. Arrays.copyOfRange(firstPacket, 510 + 255, 510 + 255 + 255), // Fourth page (continued packet). - TestData.buildOggHeader(0x05, 10, 1003, 0x01), + OggTestData.buildOggHeader(0x05, 10, 1003, 0x01), TestUtil.createByteArray(0x08), // Laces. Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8)), true); @@ -174,10 +175,10 @@ public final class OggPacketTest extends InstrumentationTestCase { public void testReadDiscardContinuedPacketAtStart() throws Exception { byte[] pageBody = TestUtil.buildTestData(256 + 8); - FakeExtractorInput input = TestData.createInput( + FakeExtractorInput input = OggTestData.createInput( TestUtil.joinByteArrays( // Page with a continued packet at start. - TestData.buildOggHeader(0x01, 10, 1001, 0x03), + OggTestData.buildOggHeader(0x01, 10, 1001, 0x03), TestUtil.createByteArray(255, 1, 8), // Laces. pageBody), true); @@ -191,15 +192,15 @@ public final class OggPacketTest extends InstrumentationTestCase { byte[] secondPacket = TestUtil.buildTestData(8, random); byte[] thirdPacket = TestUtil.buildTestData(8, random); - FakeExtractorInput input = TestData.createInput( + FakeExtractorInput input = OggTestData.createInput( TestUtil.joinByteArrays( - TestData.buildOggHeader(0x02, 0, 1000, 0x01), + OggTestData.buildOggHeader(0x02, 0, 1000, 0x01), TestUtil.createByteArray(0x08), // Laces. firstPacket, - TestData.buildOggHeader(0x04, 0, 1001, 0x03), + OggTestData.buildOggHeader(0x04, 0, 1001, 0x03), TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces. secondPacket, - TestData.buildOggHeader(0x04, 0, 1002, 0x03), + OggTestData.buildOggHeader(0x04, 0, 1002, 0x03), TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces. thirdPacket), true); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java index d5d187ee7c..6d839a8355 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import com.google.android.exoplayer2.testutil.OggTestData; import com.google.android.exoplayer2.testutil.TestUtil; import java.util.ArrayList; import java.util.Random; @@ -68,7 +69,7 @@ import junit.framework.Assert; } granule += random.nextInt(MAX_GRANULES_IN_PAGE - 1) + 1; int pageSegmentCount = random.nextInt(MAX_SEGMENT_COUNT); - byte[] header = TestData.buildOggHeader(headerType, granule, 0, pageSegmentCount); + byte[] header = OggTestData.buildOggHeader(headerType, granule, 0, pageSegmentCount); fileData.add(header); fileSize += header.length; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java deleted file mode 100644 index a24cb1599b..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.extractor.ogg; - -import com.google.android.exoplayer2.testutil.TestUtil; -import junit.framework.TestCase; - -/** - * Unit test for {@link VorbisBitArray}. - */ -public final class VorbisBitArrayTest extends TestCase { - - public void testReadBit() { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x5c, 0x50)); - assertFalse(bitArray.readBit()); - assertFalse(bitArray.readBit()); - assertTrue(bitArray.readBit()); - assertTrue(bitArray.readBit()); - assertTrue(bitArray.readBit()); - assertFalse(bitArray.readBit()); - assertTrue(bitArray.readBit()); - assertFalse(bitArray.readBit()); - assertFalse(bitArray.readBit()); - assertFalse(bitArray.readBit()); - assertFalse(bitArray.readBit()); - assertFalse(bitArray.readBit()); - assertTrue(bitArray.readBit()); - assertFalse(bitArray.readBit()); - assertTrue(bitArray.readBit()); - assertFalse(bitArray.readBit()); - } - - public void testSkipBits() { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); - bitArray.skipBits(10); - assertEquals(10, bitArray.getPosition()); - assertTrue(bitArray.readBit()); - assertTrue(bitArray.readBit()); - assertFalse(bitArray.readBit()); - bitArray.skipBits(1); - assertEquals(14, bitArray.getPosition()); - assertFalse(bitArray.readBit()); - assertFalse(bitArray.readBit()); - } - - public void testGetPosition() throws Exception { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); - assertEquals(0, bitArray.getPosition()); - bitArray.readBit(); - assertEquals(1, bitArray.getPosition()); - bitArray.readBit(); - bitArray.readBit(); - bitArray.skipBits(4); - assertEquals(7, bitArray.getPosition()); - } - - public void testSetPosition() throws Exception { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); - assertEquals(0, bitArray.getPosition()); - bitArray.setPosition(4); - assertEquals(4, bitArray.getPosition()); - bitArray.setPosition(15); - assertFalse(bitArray.readBit()); - } - - public void testReadInt32() { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F, 0xF0, 0x0F)); - assertEquals(0x0FF00FF0, bitArray.readBits(32)); - bitArray = new VorbisBitArray(TestUtil.createByteArray(0x0F, 0xF0, 0x0F, 0xF0)); - assertEquals(0xF00FF00F, bitArray.readBits(32)); - } - - public void testReadBits() throws Exception { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x03, 0x22)); - assertEquals(3, bitArray.readBits(2)); - bitArray.skipBits(6); - assertEquals(2, bitArray.readBits(2)); - bitArray.skipBits(2); - assertEquals(2, bitArray.readBits(2)); - bitArray.reset(); - assertEquals(0x2203, bitArray.readBits(16)); - } - - public void testRead4BitsBeyondBoundary() throws Exception { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x2e, 0x10)); - assertEquals(0x2e, bitArray.readBits(7)); - assertEquals(7, bitArray.getPosition()); - assertEquals(0x0, bitArray.readBits(4)); - } - - public void testReadBitsBeyondByteBoundaries() throws Exception { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xFF, 0x0F, 0xFF, 0x0F)); - assertEquals(0x0FFF0FFF, bitArray.readBits(32)); - - bitArray.reset(); - bitArray.skipBits(4); - assertEquals(0xF0FF, bitArray.readBits(16)); - - bitArray.reset(); - bitArray.skipBits(6); - assertEquals(0xc3F, bitArray.readBits(12)); - - bitArray.reset(); - bitArray.skipBits(6); - assertTrue(bitArray.readBit()); - assertTrue(bitArray.readBit()); - assertEquals(24, bitArray.bitsLeft()); - - bitArray.reset(); - bitArray.skipBits(10); - assertEquals(3, bitArray.readBits(5)); - assertEquals(15, bitArray.getPosition()); - } - - public void testReadBitsIllegalLengths() throws Exception { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x03, 0x22, 0x30)); - - // reading zero bits gets 0 without advancing position - // (like a zero-bit read is defined to yield zer0) - assertEquals(0, bitArray.readBits(0)); - assertEquals(0, bitArray.getPosition()); - bitArray.readBit(); - assertEquals(1, bitArray.getPosition()); - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java deleted file mode 100644 index c3165b34f6..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.extractor.ogg; - -import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.ogg.VorbisReader.VorbisSetup; -import com.google.android.exoplayer2.testutil.FakeExtractorInput; -import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; -import com.google.android.exoplayer2.util.ParsableByteArray; -import java.io.IOException; -import junit.framework.TestCase; - -/** - * Unit test for {@link VorbisReader}. - */ -public final class VorbisReaderTest extends TestCase { - - public void testReadBits() throws Exception { - assertEquals(0, VorbisReader.readBits((byte) 0x00, 2, 2)); - assertEquals(1, VorbisReader.readBits((byte) 0x02, 1, 1)); - assertEquals(15, VorbisReader.readBits((byte) 0xF0, 4, 4)); - assertEquals(1, VorbisReader.readBits((byte) 0x80, 1, 7)); - } - - public void testAppendNumberOfSamples() throws Exception { - ParsableByteArray buffer = new ParsableByteArray(4); - buffer.setLimit(0); - VorbisReader.appendNumberOfSamples(buffer, 0x01234567); - assertEquals(4, buffer.limit()); - assertEquals(0x67, buffer.data[0]); - assertEquals(0x45, buffer.data[1]); - assertEquals(0x23, buffer.data[2]); - assertEquals(0x01, buffer.data[3]); - } - - public void testReadSetupHeadersWithIOExceptions() throws IOException, InterruptedException { - byte[] data = TestData.getVorbisHeaderPages(); - ExtractorInput input = new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true) - .setSimulateUnknownLength(true).setSimulatePartialReads(true).build(); - - VorbisReader reader = new VorbisReader(); - VorbisReader.VorbisSetup vorbisSetup = readSetupHeaders(reader, input); - - assertNotNull(vorbisSetup.idHeader); - assertNotNull(vorbisSetup.commentHeader); - assertNotNull(vorbisSetup.setupHeaderData); - assertNotNull(vorbisSetup.modes); - - assertEquals(45, vorbisSetup.commentHeader.length); - assertEquals(30, vorbisSetup.idHeader.data.length); - assertEquals(3597, vorbisSetup.setupHeaderData.length); - - assertEquals(-1, vorbisSetup.idHeader.bitrateMax); - assertEquals(-1, vorbisSetup.idHeader.bitrateMin); - assertEquals(66666, vorbisSetup.idHeader.bitrateNominal); - assertEquals(512, vorbisSetup.idHeader.blockSize0); - assertEquals(1024, vorbisSetup.idHeader.blockSize1); - assertEquals(2, vorbisSetup.idHeader.channels); - assertTrue(vorbisSetup.idHeader.framingFlag); - assertEquals(22050, vorbisSetup.idHeader.sampleRate); - assertEquals(0, vorbisSetup.idHeader.version); - - assertEquals("Xiph.Org libVorbis I 20030909", vorbisSetup.commentHeader.vendor); - assertEquals(1, vorbisSetup.iLogModes); - - assertEquals(data[data.length - 1], - vorbisSetup.setupHeaderData[vorbisSetup.setupHeaderData.length - 1]); - - assertFalse(vorbisSetup.modes[0].blockFlag); - assertTrue(vorbisSetup.modes[1].blockFlag); - } - - private static VorbisSetup readSetupHeaders(VorbisReader reader, ExtractorInput input) - throws IOException, InterruptedException { - OggPacket oggPacket = new OggPacket(); - while (true) { - try { - if (!oggPacket.populate(input)) { - fail(); - } - VorbisSetup vorbisSetup = reader.readSetupHeaders(oggPacket.getPayload()); - if (vorbisSetup != null) { - return vorbisSetup; - } - } catch (SimulatedIOException e) { - // Ignore. - } - } - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java deleted file mode 100644 index ddbfee8446..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.extractor.ogg; - -import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.util.ParsableByteArray; -import junit.framework.TestCase; - -/** - * Unit test for {@link VorbisUtil}. - */ -public final class VorbisUtilTest extends TestCase { - - public void testILog() throws Exception { - assertEquals(0, VorbisUtil.iLog(0)); - assertEquals(1, VorbisUtil.iLog(1)); - assertEquals(2, VorbisUtil.iLog(2)); - assertEquals(2, VorbisUtil.iLog(3)); - assertEquals(3, VorbisUtil.iLog(4)); - assertEquals(3, VorbisUtil.iLog(5)); - assertEquals(4, VorbisUtil.iLog(8)); - assertEquals(0, VorbisUtil.iLog(-1)); - assertEquals(0, VorbisUtil.iLog(-122)); - } - - public void testReadIdHeader() throws Exception { - byte[] data = TestData.getIdentificationHeaderData(); - ParsableByteArray headerData = new ParsableByteArray(data, data.length); - VorbisUtil.VorbisIdHeader vorbisIdHeader = - VorbisUtil.readVorbisIdentificationHeader(headerData); - - assertEquals(22050, vorbisIdHeader.sampleRate); - assertEquals(0, vorbisIdHeader.version); - assertTrue(vorbisIdHeader.framingFlag); - assertEquals(2, vorbisIdHeader.channels); - assertEquals(512, vorbisIdHeader.blockSize0); - assertEquals(1024, vorbisIdHeader.blockSize1); - assertEquals(-1, vorbisIdHeader.bitrateMax); - assertEquals(-1, vorbisIdHeader.bitrateMin); - assertEquals(66666, vorbisIdHeader.bitrateNominal); - assertEquals(66666, vorbisIdHeader.getApproximateBitrate()); - } - - public void testReadCommentHeader() throws ParserException { - byte[] data = TestData.getCommentHeaderDataUTF8(); - ParsableByteArray headerData = new ParsableByteArray(data, data.length); - VorbisUtil.CommentHeader commentHeader = VorbisUtil.readVorbisCommentHeader(headerData); - - assertEquals("Xiph.Org libVorbis I 20120203 (Omnipresent)", commentHeader.vendor); - assertEquals(3, commentHeader.comments.length); - assertEquals("ALBUM=äö", commentHeader.comments[0]); - assertEquals("TITLE=A sample song", commentHeader.comments[1]); - assertEquals("ARTIST=Google", commentHeader.comments[2]); - } - - public void testReadVorbisModes() throws ParserException { - byte[] data = TestData.getSetupHeaderData(); - ParsableByteArray headerData = new ParsableByteArray(data, data.length); - VorbisUtil.Mode[] modes = VorbisUtil.readVorbisModes(headerData, 2); - - assertEquals(2, modes.length); - assertEquals(false, modes[0].blockFlag); - assertEquals(0, modes[0].mapping); - assertEquals(0, modes[0].transformType); - assertEquals(0, modes[0].windowType); - assertEquals(true, modes[1].blockFlag); - assertEquals(1, modes[1].mapping); - assertEquals(0, modes[1].transformType); - assertEquals(0, modes[1].windowType); - } - - public void testVerifyVorbisHeaderCapturePattern() throws ParserException { - ParsableByteArray header = new ParsableByteArray( - new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); - assertEquals(true, VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, false)); - } - - public void testVerifyVorbisHeaderCapturePatternInvalidHeader() { - ParsableByteArray header = new ParsableByteArray( - new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); - try { - VorbisUtil.verifyVorbisHeaderCapturePattern(0x99, header, false); - fail(); - } catch (ParserException e) { - assertEquals("expected header type 99", e.getMessage()); - } - } - - public void testVerifyVorbisHeaderCapturePatternInvalidHeaderQuite() throws ParserException { - ParsableByteArray header = new ParsableByteArray( - new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); - assertFalse(VorbisUtil.verifyVorbisHeaderCapturePattern(0x99, header, true)); - } - - public void testVerifyVorbisHeaderCapturePatternInvalidPattern() { - ParsableByteArray header = new ParsableByteArray( - new byte[] {0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'}); - try { - VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, false); - fail(); - } catch (ParserException e) { - assertEquals("expected characters 'vorbis'", e.getMessage()); - } - } - - public void testVerifyVorbisHeaderCapturePatternQuiteInvalidPatternQuite() - throws ParserException { - ParsableByteArray header = new ParsableByteArray( - new byte[] {0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'}); - assertFalse(VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, true)); - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java deleted file mode 100644 index 1690371a47..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.text.ttml; - -import android.graphics.Color; -import android.test.InstrumentationTestCase; - -/** - * Unit test for {@link TtmlStyle}. - */ -public final class TtmlStyleTest extends InstrumentationTestCase { - - private static final String FONT_FAMILY = "serif"; - private static final String ID = "id"; - public static final int FOREGROUND_COLOR = Color.WHITE; - public static final int BACKGROUND_COLOR = Color.BLACK; - private TtmlStyle style; - - @Override - public void setUp() throws Exception { - super.setUp(); - style = new TtmlStyle(); - } - - public void testInheritStyle() { - style.inherit(createAncestorStyle()); - assertNull("id must not be inherited", style.getId()); - assertTrue(style.isUnderline()); - assertTrue(style.isLinethrough()); - assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle()); - assertEquals(FONT_FAMILY, style.getFontFamily()); - assertEquals(Color.WHITE, style.getFontColor()); - assertFalse("do not inherit backgroundColor", style.hasBackgroundColor()); - } - - public void testChainStyle() { - style.chain(createAncestorStyle()); - assertNull("id must not be inherited", style.getId()); - assertTrue(style.isUnderline()); - assertTrue(style.isLinethrough()); - assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle()); - assertEquals(FONT_FAMILY, style.getFontFamily()); - assertEquals(FOREGROUND_COLOR, style.getFontColor()); - // do inherit backgroundColor when chaining - assertEquals("do not inherit backgroundColor when chaining", - BACKGROUND_COLOR, style.getBackgroundColor()); - } - - private TtmlStyle createAncestorStyle() { - TtmlStyle ancestor = new TtmlStyle(); - ancestor.setId(ID); - ancestor.setItalic(true); - ancestor.setBold(true); - ancestor.setBackgroundColor(BACKGROUND_COLOR); - ancestor.setFontColor(FOREGROUND_COLOR); - ancestor.setLinethrough(true); - ancestor.setUnderline(true); - ancestor.setFontFamily(FONT_FAMILY); - return ancestor; - } - - public void testStyle() { - assertEquals(TtmlStyle.UNSPECIFIED, style.getStyle()); - style.setItalic(true); - assertEquals(TtmlStyle.STYLE_ITALIC, style.getStyle()); - style.setBold(true); - assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle()); - style.setItalic(false); - assertEquals(TtmlStyle.STYLE_BOLD, style.getStyle()); - style.setBold(false); - assertEquals(TtmlStyle.STYLE_NORMAL, style.getStyle()); - } - - public void testLinethrough() { - assertFalse(style.isLinethrough()); - style.setLinethrough(true); - assertTrue(style.isLinethrough()); - style.setLinethrough(false); - assertFalse(style.isLinethrough()); - } - - public void testUnderline() { - assertFalse(style.isUnderline()); - style.setUnderline(true); - assertTrue(style.isUnderline()); - style.setUnderline(false); - assertFalse(style.isUnderline()); - } - - public void testFontFamily() { - assertNull(style.getFontFamily()); - style.setFontFamily(FONT_FAMILY); - assertEquals(FONT_FAMILY, style.getFontFamily()); - style.setFontFamily(null); - assertNull(style.getFontFamily()); - } - - public void testColor() { - assertFalse(style.hasFontColor()); - style.setFontColor(Color.BLACK); - assertEquals(Color.BLACK, style.getFontColor()); - assertTrue(style.hasFontColor()); - } - - public void testBackgroundColor() { - assertFalse(style.hasBackgroundColor()); - style.setBackgroundColor(Color.BLACK); - assertEquals(Color.BLACK, style.getBackgroundColor()); - assertTrue(style.hasBackgroundColor()); - } - - public void testId() { - assertNull(style.getId()); - style.setId(ID); - assertEquals(ID, style.getId()); - style.setId(null); - assertNull(style.getId()); - } -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java index f2e199578c..472b5c724b 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java @@ -17,11 +17,11 @@ package com.google.android.exoplayer2.upstream.cache; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.ChunkIndex; -import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; /** * Tests for {@link CachedRegionTracker}. @@ -46,10 +46,9 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - TestUtil.setUpMockito(this); + setUpMockito(this); tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); - cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } @@ -124,4 +123,14 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { return SimpleCacheSpanTest.createCacheSpan(index, cacheDir, CACHE_KEY, position, length, 0); } + /** + * Sets up Mockito for an instrumentation test. + */ + private static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { + // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. + System.setProperty("dexmaker.dexcache", + instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath()); + MockitoAnnotations.initMocks(instrumentationTestCase); + } + } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java similarity index 61% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java index 8a2c24beba..b6c068c218 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java @@ -16,20 +16,27 @@ package com.google.android.exoplayer2.drm; import static com.google.android.exoplayer2.C.PLAYREADY_UUID; +import static com.google.android.exoplayer2.C.UUID_NIL; import static com.google.android.exoplayer2.C.WIDEVINE_UUID; import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_MP4; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import android.os.Parcel; -import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.testutil.TestUtil; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link DrmInitData}. */ -public class DrmInitDataTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class DrmInitDataTest { private static final SchemeData DATA_1 = new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */)); @@ -42,6 +49,7 @@ public class DrmInitDataTest extends TestCase { private static final SchemeData DATA_UNIVERSAL = new SchemeData(C.UUID_NIL, VIDEO_MP4, TestUtil.buildTestData(128, 3 /* data seed */)); + @Test public void testParcelable() { DrmInitData drmInitDataToParcel = new DrmInitData(DATA_1, DATA_2); @@ -50,69 +58,72 @@ public class DrmInitDataTest extends TestCase { parcel.setDataPosition(0); DrmInitData drmInitDataFromParcel = DrmInitData.CREATOR.createFromParcel(parcel); - assertEquals(drmInitDataToParcel, drmInitDataFromParcel); + assertThat(drmInitDataFromParcel).isEqualTo(drmInitDataToParcel); parcel.recycle(); } + @Test public void testEquals() { DrmInitData drmInitData = new DrmInitData(DATA_1, DATA_2); // Basic non-referential equality test. DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2); - assertEquals(drmInitData, testInitData); - assertEquals(drmInitData.hashCode(), testInitData.hashCode()); + assertThat(testInitData).isEqualTo(drmInitData); + assertThat(testInitData.hashCode()).isEqualTo(drmInitData.hashCode()); // Basic non-referential equality test with non-referential scheme data. testInitData = new DrmInitData(DATA_1B, DATA_2B); - assertEquals(drmInitData, testInitData); - assertEquals(drmInitData.hashCode(), testInitData.hashCode()); + assertThat(testInitData).isEqualTo(drmInitData); + assertThat(testInitData.hashCode()).isEqualTo(drmInitData.hashCode()); // Passing the scheme data in reverse order shouldn't affect equality. testInitData = new DrmInitData(DATA_2, DATA_1); - assertEquals(drmInitData, testInitData); - assertEquals(drmInitData.hashCode(), testInitData.hashCode()); + assertThat(testInitData).isEqualTo(drmInitData); + assertThat(testInitData.hashCode()).isEqualTo(drmInitData.hashCode()); // Ditto. testInitData = new DrmInitData(DATA_2B, DATA_1B); - assertEquals(drmInitData, testInitData); - assertEquals(drmInitData.hashCode(), testInitData.hashCode()); + assertThat(testInitData).isEqualTo(drmInitData); + assertThat(testInitData.hashCode()).isEqualTo(drmInitData.hashCode()); // Different number of tuples should affect equality. testInitData = new DrmInitData(DATA_1); - MoreAsserts.assertNotEqual(drmInitData, testInitData); + assertThat(drmInitData).isNotEqualTo(testInitData); // Different data in one of the tuples should affect equality. testInitData = new DrmInitData(DATA_1, DATA_UNIVERSAL); - MoreAsserts.assertNotEqual(drmInitData, testInitData); + assertThat(testInitData).isNotEqualTo(drmInitData); } + @Test public void testGet() { // Basic matching. DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2); - assertEquals(DATA_1, testInitData.get(WIDEVINE_UUID)); - assertEquals(DATA_2, testInitData.get(PLAYREADY_UUID)); - assertNull(testInitData.get(C.UUID_NIL)); + assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1); + assertThat(testInitData.get(PLAYREADY_UUID)).isEqualTo(DATA_2); + assertThat(testInitData.get(UUID_NIL)).isNull(); // Basic matching including universal data. testInitData = new DrmInitData(DATA_1, DATA_2, DATA_UNIVERSAL); - assertEquals(DATA_1, testInitData.get(WIDEVINE_UUID)); - assertEquals(DATA_2, testInitData.get(PLAYREADY_UUID)); - assertEquals(DATA_UNIVERSAL, testInitData.get(C.UUID_NIL)); + assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1); + assertThat(testInitData.get(PLAYREADY_UUID)).isEqualTo(DATA_2); + assertThat(testInitData.get(UUID_NIL)).isEqualTo(DATA_UNIVERSAL); // Passing the scheme data in reverse order shouldn't affect equality. testInitData = new DrmInitData(DATA_UNIVERSAL, DATA_2, DATA_1); - assertEquals(DATA_1, testInitData.get(WIDEVINE_UUID)); - assertEquals(DATA_2, testInitData.get(PLAYREADY_UUID)); - assertEquals(DATA_UNIVERSAL, testInitData.get(C.UUID_NIL)); + assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1); + assertThat(testInitData.get(PLAYREADY_UUID)).isEqualTo(DATA_2); + assertThat(testInitData.get(UUID_NIL)).isEqualTo(DATA_UNIVERSAL); // Universal data should be returned in the absence of a specific match. testInitData = new DrmInitData(DATA_1, DATA_UNIVERSAL); - assertEquals(DATA_1, testInitData.get(WIDEVINE_UUID)); - assertEquals(DATA_UNIVERSAL, testInitData.get(PLAYREADY_UUID)); - assertEquals(DATA_UNIVERSAL, testInitData.get(C.UUID_NIL)); + assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1); + assertThat(testInitData.get(PLAYREADY_UUID)).isEqualTo(DATA_UNIVERSAL); + assertThat(testInitData.get(UUID_NIL)).isEqualTo(DATA_UNIVERSAL); } + @Test public void testDuplicateSchemeDataRejected() { try { new DrmInitData(DATA_1, DATA_1); @@ -136,18 +147,19 @@ public class DrmInitDataTest extends TestCase { } } + @Test public void testSchemeDataMatches() { - assertTrue(DATA_1.matches(WIDEVINE_UUID)); - assertFalse(DATA_1.matches(PLAYREADY_UUID)); - assertFalse(DATA_2.matches(C.UUID_NIL)); + assertThat(DATA_1.matches(WIDEVINE_UUID)).isTrue(); + assertThat(DATA_1.matches(PLAYREADY_UUID)).isFalse(); + assertThat(DATA_2.matches(UUID_NIL)).isFalse(); - assertFalse(DATA_2.matches(WIDEVINE_UUID)); - assertTrue(DATA_2.matches(PLAYREADY_UUID)); - assertFalse(DATA_2.matches(C.UUID_NIL)); + assertThat(DATA_2.matches(WIDEVINE_UUID)).isFalse(); + assertThat(DATA_2.matches(PLAYREADY_UUID)).isTrue(); + assertThat(DATA_2.matches(UUID_NIL)).isFalse(); - assertTrue(DATA_UNIVERSAL.matches(WIDEVINE_UUID)); - assertTrue(DATA_UNIVERSAL.matches(PLAYREADY_UUID)); - assertTrue(DATA_UNIVERSAL.matches(C.UUID_NIL)); + assertThat(DATA_UNIVERSAL.matches(WIDEVINE_UUID)).isTrue(); + assertThat(DATA_UNIVERSAL.matches(PLAYREADY_UUID)).isTrue(); + assertThat(DATA_UNIVERSAL.matches(UUID_NIL)).isTrue(); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java similarity index 78% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java index 6abd116086..8e27c4f7ca 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java @@ -15,6 +15,12 @@ */ package com.google.android.exoplayer2.extractor; +import static com.google.android.exoplayer2.C.RESULT_END_OF_INPUT; +import static com.google.common.truth.Truth.assertThat; +import static java.util.Arrays.copyOf; +import static java.util.Arrays.copyOfRange; +import static org.junit.Assert.fail; + import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeDataSource; @@ -22,42 +28,50 @@ import com.google.android.exoplayer2.upstream.DataSpec; import java.io.EOFException; import java.io.IOException; import java.util.Arrays; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link DefaultExtractorInput}. */ -public class DefaultExtractorInputTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class DefaultExtractorInputTest { private static final String TEST_URI = "http://www.google.com"; private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8}; private static final int LARGE_TEST_DATA_LENGTH = 8192; + @Test public void testInitialPosition() throws Exception { FakeDataSource testDataSource = buildDataSource(); DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 123, C.LENGTH_UNSET); - assertEquals(123, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(123); } + @Test public void testRead() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; // We expect to perform three reads of three bytes, as setup in buildTestDataSource. int bytesRead = 0; bytesRead += input.read(target, 0, TEST_DATA.length); - assertEquals(3, bytesRead); + assertThat(bytesRead).isEqualTo(3); bytesRead += input.read(target, 3, TEST_DATA.length); - assertEquals(6, bytesRead); + assertThat(bytesRead).isEqualTo(6); bytesRead += input.read(target, 6, TEST_DATA.length); - assertEquals(9, bytesRead); + assertThat(bytesRead).isEqualTo(9); // Check the read data is correct. - assertTrue(Arrays.equals(TEST_DATA, target)); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); // Check we're now indicated that the end of input is reached. int expectedEndOfInput = input.read(target, 0, TEST_DATA.length); - assertEquals(C.RESULT_END_OF_INPUT, expectedEndOfInput); + assertThat(expectedEndOfInput).isEqualTo(RESULT_END_OF_INPUT); } + @Test public void testReadPeeked() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; @@ -65,12 +79,13 @@ public class DefaultExtractorInputTest extends TestCase { input.advancePeekPosition(TEST_DATA.length); int bytesRead = input.read(target, 0, TEST_DATA.length); - assertEquals(TEST_DATA.length, bytesRead); + assertThat(bytesRead).isEqualTo(TEST_DATA.length); // Check the read data is correct. - assertTrue(Arrays.equals(TEST_DATA, target)); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); } + @Test public void testReadMoreDataPeeked() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; @@ -78,22 +93,23 @@ public class DefaultExtractorInputTest extends TestCase { input.advancePeekPosition(TEST_DATA.length); int bytesRead = input.read(target, 0, TEST_DATA.length + 1); - assertEquals(TEST_DATA.length, bytesRead); + assertThat(bytesRead).isEqualTo(TEST_DATA.length); // Check the read data is correct. - assertTrue(Arrays.equals(TEST_DATA, target)); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); } + @Test public void testReadFullyOnce() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; input.readFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertTrue(Arrays.equals(TEST_DATA, target)); - assertEquals(TEST_DATA.length, input.getPosition()); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); // Check that we see end of input if we read again with allowEndOfInput set. boolean result = input.readFully(target, 0, 1, true); - assertFalse(result); + assertThat(result).isFalse(); // Check that we fail with EOFException we read again with allowEndOfInput unset. try { input.readFully(target, 0, 1); @@ -103,19 +119,21 @@ public class DefaultExtractorInputTest extends TestCase { } } + @Test public void testReadFullyTwice() throws Exception { // Read TEST_DATA in two parts. DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[5]; input.readFully(target, 0, 5); - assertTrue(Arrays.equals(Arrays.copyOf(TEST_DATA, 5), target)); - assertEquals(5, input.getPosition()); + assertThat(Arrays.equals(copyOf(TEST_DATA, 5), target)).isTrue(); + assertThat(input.getPosition()).isEqualTo(5); target = new byte[4]; input.readFully(target, 0, 4); - assertTrue(Arrays.equals(Arrays.copyOfRange(TEST_DATA, 5, 9), target)); - assertEquals(5 + 4, input.getPosition()); + assertThat(Arrays.equals(copyOfRange(TEST_DATA, 5, 9), target)).isTrue(); + assertThat(input.getPosition()).isEqualTo(5 + 4); } + @Test public void testReadFullyTooMuch() throws Exception { // Read more than TEST_DATA. Should fail with an EOFException. Position should not update. DefaultExtractorInput input = createDefaultExtractorInput(); @@ -126,7 +144,7 @@ public class DefaultExtractorInputTest extends TestCase { } catch (EOFException e) { // Expected. } - assertEquals(0, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(0); // Read more than TEST_DATA with allowEndOfInput set. Should fail with an EOFException because // the end of input isn't encountered immediately. Position should not update. @@ -138,9 +156,10 @@ public class DefaultExtractorInputTest extends TestCase { } catch (EOFException e) { // Expected. } - assertEquals(0, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(0); } + @Test public void testReadFullyWithFailingDataSource() throws Exception { FakeDataSource testDataSource = buildFailingDataSource(); DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); @@ -152,9 +171,10 @@ public class DefaultExtractorInputTest extends TestCase { // Expected. } // The position should not have advanced. - assertEquals(0, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(0); } + @Test public void testReadFullyHalfPeeked() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; @@ -164,22 +184,24 @@ public class DefaultExtractorInputTest extends TestCase { input.readFully(target, 0, TEST_DATA.length); // Check the read data is correct. - assertTrue(Arrays.equals(TEST_DATA, target)); - assertEquals(TEST_DATA.length, input.getPosition()); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); } + @Test public void testSkip() throws Exception { FakeDataSource testDataSource = buildDataSource(); DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); // We expect to perform three skips of three bytes, as setup in buildTestDataSource. for (int i = 0; i < 3; i++) { - assertEquals(3, input.skip(TEST_DATA.length)); + assertThat(input.skip(TEST_DATA.length)).isEqualTo(3); } // Check we're now indicated that the end of input is reached. int expectedEndOfInput = input.skip(TEST_DATA.length); - assertEquals(C.RESULT_END_OF_INPUT, expectedEndOfInput); + assertThat(expectedEndOfInput).isEqualTo(RESULT_END_OF_INPUT); } + @Test public void testLargeSkip() throws Exception { FakeDataSource testDataSource = buildLargeDataSource(); DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); @@ -190,14 +212,15 @@ public class DefaultExtractorInputTest extends TestCase { } } + @Test public void testSkipFullyOnce() throws Exception { // Skip TEST_DATA. DefaultExtractorInput input = createDefaultExtractorInput(); input.skipFully(TEST_DATA.length); - assertEquals(TEST_DATA.length, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); // Check that we see end of input if we skip again with allowEndOfInput set. boolean result = input.skipFully(1, true); - assertFalse(result); + assertThat(result).isFalse(); // Check that we fail with EOFException we skip again. try { input.skipFully(1); @@ -207,15 +230,17 @@ public class DefaultExtractorInputTest extends TestCase { } } + @Test public void testSkipFullyTwice() throws Exception { // Skip TEST_DATA in two parts. DefaultExtractorInput input = createDefaultExtractorInput(); input.skipFully(5); - assertEquals(5, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(5); input.skipFully(4); - assertEquals(5 + 4, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(5 + 4); } + @Test public void testSkipFullyTwicePeeked() throws Exception { // Skip TEST_DATA. DefaultExtractorInput input = createDefaultExtractorInput(); @@ -224,12 +249,13 @@ public class DefaultExtractorInputTest extends TestCase { int halfLength = TEST_DATA.length / 2; input.skipFully(halfLength); - assertEquals(halfLength, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(halfLength); input.skipFully(TEST_DATA.length - halfLength); - assertEquals(TEST_DATA.length, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); } + @Test public void testSkipFullyTooMuch() throws Exception { // Skip more than TEST_DATA. Should fail with an EOFException. Position should not update. DefaultExtractorInput input = createDefaultExtractorInput(); @@ -239,7 +265,7 @@ public class DefaultExtractorInputTest extends TestCase { } catch (EOFException e) { // Expected. } - assertEquals(0, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(0); // Skip more than TEST_DATA with allowEndOfInput set. Should fail with an EOFException because // the end of input isn't encountered immediately. Position should not update. @@ -250,9 +276,10 @@ public class DefaultExtractorInputTest extends TestCase { } catch (EOFException e) { // Expected. } - assertEquals(0, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(0); } + @Test public void testSkipFullyWithFailingDataSource() throws Exception { FakeDataSource testDataSource = buildFailingDataSource(); DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); @@ -263,9 +290,10 @@ public class DefaultExtractorInputTest extends TestCase { // Expected. } // The position should not have advanced. - assertEquals(0, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(0); } + @Test public void testSkipFullyLarge() throws Exception { // Tests skipping an amount of data that's larger than any internal scratch space. int largeSkipSize = 1024 * 1024; @@ -275,7 +303,7 @@ public class DefaultExtractorInputTest extends TestCase { DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); input.skipFully(largeSkipSize); - assertEquals(largeSkipSize, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(largeSkipSize); // Check that we fail with EOFException we skip again. try { input.skipFully(1); @@ -285,22 +313,23 @@ public class DefaultExtractorInputTest extends TestCase { } } + @Test public void testPeekFully() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; input.peekFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertTrue(Arrays.equals(TEST_DATA, target)); - assertEquals(0, input.getPosition()); - assertEquals(TEST_DATA.length, input.getPeekPosition()); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); // Check that we can read again from the buffer byte[] target2 = new byte[TEST_DATA.length]; input.readFully(target2, 0, TEST_DATA.length); - assertTrue(Arrays.equals(TEST_DATA, target2)); - assertEquals(TEST_DATA.length, input.getPosition()); - assertEquals(TEST_DATA.length, input.getPeekPosition()); + assertThat(Arrays.equals(TEST_DATA, target2)).isTrue(); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); + assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); // Check that we fail with EOFException if we peek again try { @@ -311,20 +340,21 @@ public class DefaultExtractorInputTest extends TestCase { } } + @Test public void testResetPeekPosition() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; input.peekFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertTrue(Arrays.equals(TEST_DATA, target)); - assertEquals(0, input.getPosition()); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(input.getPosition()).isEqualTo(0); // Check that we can peek again after resetting. input.resetPeekPosition(); byte[] target2 = new byte[TEST_DATA.length]; input.peekFully(target2, 0, TEST_DATA.length); - assertTrue(Arrays.equals(TEST_DATA, target2)); + assertThat(Arrays.equals(TEST_DATA, target2)).isTrue(); // Check that we fail with EOFException if we peek past the end of the input. try { @@ -335,40 +365,43 @@ public class DefaultExtractorInputTest extends TestCase { } } + @Test public void testPeekFullyAtEndOfStreamWithAllowEndOfInputSucceeds() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; // Check peeking up to the end of input succeeds. - assertTrue(input.peekFully(target, 0, TEST_DATA.length, true)); + assertThat(input.peekFully(target, 0, TEST_DATA.length, true)).isTrue(); // Check peeking at the end of input with allowEndOfInput signals the end of input. - assertFalse(input.peekFully(target, 0, 1, true)); + assertThat(input.peekFully(target, 0, 1, true)).isFalse(); } + @Test public void testPeekFullyAtEndThenReadEndOfInput() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; // Peek up to the end of the input. - assertTrue(input.peekFully(target, 0, TEST_DATA.length, false)); + assertThat(input.peekFully(target, 0, TEST_DATA.length, false)).isTrue(); // Peek the end of the input. - assertFalse(input.peekFully(target, 0, 1, true)); + assertThat(input.peekFully(target, 0, 1, true)).isFalse(); // Read up to the end of the input. - assertTrue(input.readFully(target, 0, TEST_DATA.length, false)); + assertThat(input.readFully(target, 0, TEST_DATA.length, false)).isTrue(); // Read the end of the input. - assertFalse(input.readFully(target, 0, 1, true)); + assertThat(input.readFully(target, 0, 1, true)).isFalse(); } + @Test public void testPeekFullyAcrossEndOfInputWithAllowEndOfInputFails() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; // Check peeking before the end of input with allowEndOfInput succeeds. - assertTrue(input.peekFully(target, 0, TEST_DATA.length - 1, true)); + assertThat(input.peekFully(target, 0, TEST_DATA.length - 1, true)).isTrue(); // Check peeking across the end of input with allowEndOfInput throws. try { @@ -379,12 +412,13 @@ public class DefaultExtractorInputTest extends TestCase { } } + @Test public void testResetAndPeekFullyPastEndOfStreamWithAllowEndOfInputFails() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; // Check peeking up to the end of input succeeds. - assertTrue(input.peekFully(target, 0, TEST_DATA.length, true)); + assertThat(input.peekFully(target, 0, TEST_DATA.length, true)).isTrue(); input.resetPeekPosition(); try { // Check peeking one more byte throws. diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ExtractorTest.java similarity index 60% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ExtractorTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ExtractorTest.java index 250ae8c513..fc31a7be73 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ExtractorTest.java @@ -15,20 +15,28 @@ */ package com.google.android.exoplayer2.extractor; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.C; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link Extractor}. */ -public class ExtractorTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ExtractorTest { - public static void testConstants() { + @Test + public void testConstants() { // Sanity check that constant values match those defined by {@link C}. - assertEquals(C.RESULT_END_OF_INPUT, Extractor.RESULT_END_OF_INPUT); + assertThat(Extractor.RESULT_END_OF_INPUT).isEqualTo(C.RESULT_END_OF_INPUT); // Sanity check that the other constant values don't overlap. - assertTrue(C.RESULT_END_OF_INPUT != Extractor.RESULT_CONTINUE); - assertTrue(C.RESULT_END_OF_INPUT != Extractor.RESULT_SEEK); + assertThat(C.RESULT_END_OF_INPUT != Extractor.RESULT_CONTINUE).isTrue(); + assertThat(C.RESULT_END_OF_INPUT != Extractor.RESULT_SEEK).isTrue(); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java similarity index 92% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java index acc62f41f9..708ffde080 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.extractor.mkv; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.TestUtil; @@ -22,13 +24,19 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Tests {@link DefaultEbmlReader}. */ -public class DefaultEbmlReaderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class DefaultEbmlReaderTest { + @Test public void testMasterElement() throws IOException, InterruptedException { ExtractorInput input = createTestInput(0x1A, 0x45, 0xDF, 0xA3, 0x84, 0x42, 0x85, 0x81, 0x01); TestOutput expected = new TestOutput(); @@ -38,6 +46,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testMasterElementEmpty() throws IOException, InterruptedException { ExtractorInput input = createTestInput(0x18, 0x53, 0x80, 0x67, 0x80); TestOutput expected = new TestOutput(); @@ -46,6 +55,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testUnsignedIntegerElement() throws IOException, InterruptedException { // 0xFE is chosen because for signed integers it should be interpreted as -2 ExtractorInput input = createTestInput(0x42, 0xF7, 0x81, 0xFE); @@ -54,6 +64,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testUnsignedIntegerElementLarge() throws IOException, InterruptedException { ExtractorInput input = createTestInput(0x42, 0xF7, 0x88, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF); @@ -62,6 +73,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testUnsignedIntegerElementTooLargeBecomesNegative() throws IOException, InterruptedException { ExtractorInput input = @@ -71,6 +83,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testStringElement() throws IOException, InterruptedException { ExtractorInput input = createTestInput(0x42, 0x82, 0x86, 0x41, 0x62, 0x63, 0x31, 0x32, 0x33); TestOutput expected = new TestOutput(); @@ -78,6 +91,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testStringElementEmpty() throws IOException, InterruptedException { ExtractorInput input = createTestInput(0x42, 0x82, 0x80); TestOutput expected = new TestOutput(); @@ -85,6 +99,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testFloatElementFourBytes() throws IOException, InterruptedException { ExtractorInput input = createTestInput(0x44, 0x89, 0x84, 0x3F, 0x80, 0x00, 0x00); @@ -93,6 +108,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testFloatElementEightBytes() throws IOException, InterruptedException { ExtractorInput input = createTestInput(0x44, 0x89, 0x88, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00); @@ -101,6 +117,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testBinaryElement() throws IOException, InterruptedException { ExtractorInput input = createTestInput(0xA3, 0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08); @@ -118,16 +135,16 @@ public class DefaultEbmlReaderTest extends TestCase { // We expect the number of successful reads to equal the number of expected events. for (int i = 0; i < expectedEvents.size(); i++) { - assertTrue(reader.read(input)); + assertThat(reader.read(input)).isTrue(); } // The next read should be unsuccessful. - assertFalse(reader.read(input)); + assertThat(reader.read(input)).isFalse(); // Check that we really did get to the end of input. - assertFalse(input.readFully(new byte[1], 0, 1, true)); + assertThat(input.readFully(new byte[1], 0, 1, true)).isFalse(); - assertEquals(expectedEvents.size(), output.events.size()); + assertThat(output.events).hasSize(expectedEvents.size()); for (int i = 0; i < expectedEvents.size(); i++) { - assertEquals(expectedEvents.get(i), output.events.get(i)); + assertThat(output.events.get(i)).isEqualTo(expectedEvents.get(i)); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java similarity index 92% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java index 3eb2c10a30..bda93db812 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java @@ -15,18 +15,28 @@ */ package com.google.android.exoplayer2.extractor.mkv; +import static com.google.android.exoplayer2.C.RESULT_END_OF_INPUT; +import static com.google.android.exoplayer2.C.RESULT_MAX_LENGTH_EXCEEDED; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; import java.io.EOFException; import java.io.IOException; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Tests for {@link VarintReader}. */ -public final class VarintReaderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class VarintReaderTest { private static final byte MAX_BYTE = (byte) 0xFF; @@ -78,6 +88,7 @@ public final class VarintReaderTest extends TestCase { private static final long VALUE_8_BYTE_MAX = 0xFFFFFFFFFFFFFFL; private static final long VALUE_8_BYTE_MAX_WITH_MASK = 0x1FFFFFFFFFFFFFFL; + @Test public void testReadVarintEndOfInputAtStart() throws IOException, InterruptedException { VarintReader reader = new VarintReader(); // Build an input with no data. @@ -86,7 +97,7 @@ public final class VarintReaderTest extends TestCase { .build(); // End of input allowed. long result = reader.readUnsignedVarint(input, true, false, 8); - assertEquals(C.RESULT_END_OF_INPUT, result); + assertThat(result).isEqualTo(RESULT_END_OF_INPUT); // End of input not allowed. try { reader.readUnsignedVarint(input, false, false, 8); @@ -96,6 +107,7 @@ public final class VarintReaderTest extends TestCase { } } + @Test public void testReadVarintExceedsMaximumAllowedLength() throws IOException, InterruptedException { VarintReader reader = new VarintReader(); ExtractorInput input = new FakeExtractorInput.Builder() @@ -103,9 +115,10 @@ public final class VarintReaderTest extends TestCase { .setSimulateUnknownLength(true) .build(); long result = reader.readUnsignedVarint(input, false, true, 4); - assertEquals(C.RESULT_MAX_LENGTH_EXCEEDED, result); + assertThat(result).isEqualTo(RESULT_MAX_LENGTH_EXCEEDED); } + @Test public void testReadVarint() throws IOException, InterruptedException { VarintReader reader = new VarintReader(); testReadVarint(reader, true, DATA_1_BYTE_0, 1, 0); @@ -142,6 +155,7 @@ public final class VarintReaderTest extends TestCase { testReadVarint(reader, false, DATA_8_BYTE_MAX, 8, VALUE_8_BYTE_MAX_WITH_MASK); } + @Test public void testReadVarintFlaky() throws IOException, InterruptedException { VarintReader reader = new VarintReader(); testReadVarintFlaky(reader, true, DATA_1_BYTE_0, 1, 0); @@ -185,8 +199,8 @@ public final class VarintReaderTest extends TestCase { .setSimulateUnknownLength(true) .build(); long result = reader.readUnsignedVarint(input, false, removeMask, 8); - assertEquals(expectedLength, input.getPosition()); - assertEquals(expectedValue, result); + assertThat(input.getPosition()).isEqualTo(expectedLength); + assertThat(result).isEqualTo(expectedValue); } private static void testReadVarintFlaky(VarintReader reader, boolean removeMask, byte[] data, @@ -209,8 +223,8 @@ public final class VarintReaderTest extends TestCase { // Expected. } } - assertEquals(expectedLength, input.getPosition()); - assertEquals(expectedValue, result); + assertThat(input.getPosition()).isEqualTo(expectedLength); + assertThat(result).isEqualTo(expectedValue); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java similarity index 67% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java index 18775b17f4..b43949b7c2 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java @@ -15,16 +15,24 @@ */ package com.google.android.exoplayer2.extractor.mp3; -import android.test.InstrumentationTestCase; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Tests for {@link XingSeeker}. */ -public final class XingSeekerTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class XingSeekerTest { // Xing header/payload from http://storage.googleapis.com/exoplayer-test-media-0/play.mp3. private static final int XING_FRAME_HEADER_DATA = 0xFFFB3000; @@ -51,7 +59,7 @@ public final class XingSeekerTest extends InstrumentationTestCase { private XingSeeker seekerWithInputLength; private int xingFrameSize; - @Override + @Before public void setUp() throws Exception { MpegAudioHeader xingFrameHeader = new MpegAudioHeader(); MpegAudioHeader.populateHeader(XING_FRAME_HEADER_DATA, xingFrameHeader); @@ -62,42 +70,49 @@ public final class XingSeekerTest extends InstrumentationTestCase { xingFrameSize = xingFrameHeader.frameSize; } + @Test public void testGetTimeUsBeforeFirstAudioFrame() { - assertEquals(0, seeker.getTimeUs(-1)); - assertEquals(0, seekerWithInputLength.getTimeUs(-1)); + assertThat(seeker.getTimeUs(-1)).isEqualTo(0); + assertThat(seekerWithInputLength.getTimeUs(-1)).isEqualTo(0); } + @Test public void testGetTimeUsAtFirstAudioFrame() { - assertEquals(0, seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize)); - assertEquals(0, seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize)); + assertThat(seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize)).isEqualTo(0); + assertThat(seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize)).isEqualTo(0); } + @Test public void testGetTimeUsAtEndOfStream() { - assertEquals(STREAM_DURATION_US, - seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)); - assertEquals(STREAM_DURATION_US, - seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)); + assertThat(seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)) + .isEqualTo(STREAM_DURATION_US); + assertThat( + seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)) + .isEqualTo(STREAM_DURATION_US); } + @Test public void testGetPositionAtStartOfStream() { - assertEquals(XING_FRAME_POSITION + xingFrameSize, seeker.getPosition(0)); - assertEquals(XING_FRAME_POSITION + xingFrameSize, seekerWithInputLength.getPosition(0)); + assertThat(seeker.getPosition(0)).isEqualTo(XING_FRAME_POSITION + xingFrameSize); + assertThat(seekerWithInputLength.getPosition(0)).isEqualTo(XING_FRAME_POSITION + xingFrameSize); } + @Test public void testGetPositionAtEndOfStream() { - assertEquals(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1, - seeker.getPosition(STREAM_DURATION_US)); - assertEquals(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1, - seekerWithInputLength.getPosition(STREAM_DURATION_US)); + assertThat(seeker.getPosition(STREAM_DURATION_US)) + .isEqualTo(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1); + assertThat(seekerWithInputLength.getPosition(STREAM_DURATION_US)) + .isEqualTo(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1); } + @Test public void testGetTimeForAllPositions() { for (int offset = xingFrameSize; offset < STREAM_SIZE_BYTES; offset++) { int position = XING_FRAME_POSITION + offset; long timeUs = seeker.getTimeUs(position); - assertEquals(position, seeker.getPosition(timeUs)); + assertThat(seeker.getPosition(timeUs)).isEqualTo(position); timeUs = seekerWithInputLength.getTimeUs(position); - assertEquals(position, seekerWithInputLength.getPosition(timeUs)); + assertThat(seekerWithInputLength.getPosition(timeUs)).isEqualTo(position); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java similarity index 79% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java index d0213337b8..0c69e0b176 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java @@ -15,14 +15,21 @@ */ package com.google.android.exoplayer2.extractor.mp4; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Tests for {@link AtomParsers}. */ -public final class AtomParsersTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class AtomParsersTest { private static final String ATOM_HEADER = "000000000000000000000000"; private static final String SAMPLE_COUNT = "00000004"; @@ -33,24 +40,27 @@ public final class AtomParsersTest extends TestCase { private static final byte[] SIXTEEN_BIT_STZ2 = Util.getBytesFromHexString(ATOM_HEADER + "00000010" + SAMPLE_COUNT + "0001000200030004"); + @Test public void testStz2Parsing4BitFieldSize() { verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(FOUR_BIT_STZ2))); } + @Test public void testStz2Parsing8BitFieldSize() { verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(EIGHT_BIT_STZ2))); } + @Test public void testStz2Parsing16BitFieldSize() { verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(SIXTEEN_BIT_STZ2))); } private void verifyParsing(Atom.LeafAtom stz2Atom) { AtomParsers.Stz2SampleSizeBox box = new AtomParsers.Stz2SampleSizeBox(stz2Atom); - assertEquals(4, box.getSampleCount()); - assertFalse(box.isFixedSampleSize()); + assertThat(box.getSampleCount()).isEqualTo(4); + assertThat(box.isFixedSampleSize()).isFalse(); for (int i = 0; i < box.getSampleCount(); i++) { - assertEquals(i + 1, box.readNextSampleSize()); + assertThat(box.readNextSampleSize()).isEqualTo(i + 1); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java similarity index 56% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java index 5ac3979746..4d7931cc02 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java @@ -15,33 +15,44 @@ */ package com.google.android.exoplayer2.extractor.mp4; -import android.test.MoreAsserts; +import static com.google.android.exoplayer2.C.WIDEVINE_UUID; +import static com.google.android.exoplayer2.extractor.mp4.Atom.TYPE_pssh; +import static com.google.android.exoplayer2.extractor.mp4.Atom.parseFullAtomFlags; +import static com.google.android.exoplayer2.extractor.mp4.Atom.parseFullAtomVersion; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.UUID; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Tests for {@link PsshAtomUtil}. */ -public class PsshAtomUtilTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class PsshAtomUtilTest { + @Test public void testBuildPsshAtom() { byte[] schemeData = new byte[]{0, 1, 2, 3, 4, 5}; byte[] psshAtom = PsshAtomUtil.buildPsshAtom(C.WIDEVINE_UUID, schemeData); // Read the PSSH atom back and assert its content is as expected. ParsableByteArray parsablePsshAtom = new ParsableByteArray(psshAtom); - assertEquals(psshAtom.length, parsablePsshAtom.readUnsignedIntToInt()); // length - assertEquals(Atom.TYPE_pssh, parsablePsshAtom.readInt()); // type + assertThat(parsablePsshAtom.readUnsignedIntToInt()).isEqualTo(psshAtom.length); // length + assertThat(parsablePsshAtom.readInt()).isEqualTo(TYPE_pssh); // type int fullAtomInt = parsablePsshAtom.readInt(); // version + flags - assertEquals(0, Atom.parseFullAtomVersion(fullAtomInt)); - assertEquals(0, Atom.parseFullAtomFlags(fullAtomInt)); + assertThat(parseFullAtomVersion(fullAtomInt)).isEqualTo(0); + assertThat(parseFullAtomFlags(fullAtomInt)).isEqualTo(0); UUID systemId = new UUID(parsablePsshAtom.readLong(), parsablePsshAtom.readLong()); - assertEquals(C.WIDEVINE_UUID, systemId); - assertEquals(schemeData.length, parsablePsshAtom.readUnsignedIntToInt()); + assertThat(systemId).isEqualTo(WIDEVINE_UUID); + assertThat(parsablePsshAtom.readUnsignedIntToInt()).isEqualTo(schemeData.length); byte[] psshSchemeData = new byte[schemeData.length]; parsablePsshAtom.readBytes(psshSchemeData, 0, schemeData.length); - MoreAsserts.assertEquals(schemeData, psshSchemeData); + assertThat(psshSchemeData).isEqualTo(schemeData); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java similarity index 72% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java index 1acc208c29..a3f7e9a548 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java @@ -15,54 +15,67 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.OggTestData; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.EOFException; import java.io.IOException; import java.util.Random; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link DefaultOggSeeker} utility methods. */ -public class DefaultOggSeekerUtilMethodsTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class DefaultOggSeekerUtilMethodsTest { private final Random random = new Random(0); + @Test public void testSkipToNextPage() throws Exception { - FakeExtractorInput extractorInput = TestData.createInput( + FakeExtractorInput extractorInput = OggTestData.createInput( TestUtil.joinByteArrays( TestUtil.buildTestData(4000, random), new byte[] {'O', 'g', 'g', 'S'}, TestUtil.buildTestData(4000, random) ), false); skipToNextPage(extractorInput); - assertEquals(4000, extractorInput.getPosition()); + assertThat(extractorInput.getPosition()).isEqualTo(4000); } + @Test public void testSkipToNextPageOverlap() throws Exception { - FakeExtractorInput extractorInput = TestData.createInput( + FakeExtractorInput extractorInput = OggTestData.createInput( TestUtil.joinByteArrays( TestUtil.buildTestData(2046, random), new byte[] {'O', 'g', 'g', 'S'}, TestUtil.buildTestData(4000, random) ), false); skipToNextPage(extractorInput); - assertEquals(2046, extractorInput.getPosition()); + assertThat(extractorInput.getPosition()).isEqualTo(2046); } + @Test public void testSkipToNextPageInputShorterThanPeekLength() throws Exception { - FakeExtractorInput extractorInput = TestData.createInput( + FakeExtractorInput extractorInput = OggTestData.createInput( TestUtil.joinByteArrays( new byte[] {'x', 'O', 'g', 'g', 'S'} ), false); skipToNextPage(extractorInput); - assertEquals(1, extractorInput.getPosition()); + assertThat(extractorInput.getPosition()).isEqualTo(1); } + @Test public void testSkipToNextPageNoMatch() throws Exception { - FakeExtractorInput extractorInput = TestData.createInput( + FakeExtractorInput extractorInput = OggTestData.createInput( new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false); try { skipToNextPage(extractorInput); @@ -84,16 +97,17 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { } } + @Test public void testSkipToPageOfGranule() throws IOException, InterruptedException { byte[] packet = TestUtil.buildTestData(3 * 254, random); byte[] data = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 20000, 1000, 0x03), + OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet, - TestData.buildOggHeader(0x04, 40000, 1001, 0x03), + OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet, - TestData.buildOggHeader(0x04, 60000, 1002, 0x03), + OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); @@ -101,44 +115,46 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { // expect to be granule of the previous page returned as elapsedSamples skipToPageOfGranule(input, 54000, 40000); // expect to be at the start of the third page - assertEquals(2 * (30 + (3 * 254)), input.getPosition()); + assertThat(input.getPosition()).isEqualTo(2 * (30 + (3 * 254))); } + @Test public void testSkipToPageOfGranulePreciseMatch() throws IOException, InterruptedException { byte[] packet = TestUtil.buildTestData(3 * 254, random); byte[] data = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 20000, 1000, 0x03), + OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet, - TestData.buildOggHeader(0x04, 40000, 1001, 0x03), + OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet, - TestData.buildOggHeader(0x04, 60000, 1002, 0x03), + OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); skipToPageOfGranule(input, 40000, 20000); // expect to be at the start of the second page - assertEquals((30 + (3 * 254)), input.getPosition()); + assertThat(input.getPosition()).isEqualTo(30 + (3 * 254)); } + @Test public void testSkipToPageOfGranuleAfterTargetPage() throws IOException, InterruptedException { byte[] packet = TestUtil.buildTestData(3 * 254, random); byte[] data = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 20000, 1000, 0x03), + OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet, - TestData.buildOggHeader(0x04, 40000, 1001, 0x03), + OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet, - TestData.buildOggHeader(0x04, 60000, 1002, 0x03), + OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); skipToPageOfGranule(input, 10000, -1); - assertEquals(0, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(0); } private void skipToPageOfGranule(ExtractorInput input, long granule, @@ -146,7 +162,8 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader(), 1, 2); while (true) { try { - assertEquals(elapsedSamplesExpected, oggSeeker.skipToPageOfGranule(input, granule, -1)); + assertThat(oggSeeker.skipToPageOfGranule(input, granule, -1)) + .isEqualTo(elapsedSamplesExpected); return; } catch (FakeExtractorInput.SimulatedIOException e) { input.resetPeekPosition(); @@ -154,24 +171,26 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { } } + @Test public void testReadGranuleOfLastPage() throws IOException, InterruptedException { - FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays( + FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays( TestUtil.buildTestData(100, random), - TestData.buildOggHeader(0x00, 20000, 66, 3), + OggTestData.buildOggHeader(0x00, 20000, 66, 3), TestUtil.createByteArray(254, 254, 254), // laces TestUtil.buildTestData(3 * 254, random), - TestData.buildOggHeader(0x00, 40000, 67, 3), + OggTestData.buildOggHeader(0x00, 40000, 67, 3), TestUtil.createByteArray(254, 254, 254), // laces TestUtil.buildTestData(3 * 254, random), - TestData.buildOggHeader(0x05, 60000, 68, 3), + OggTestData.buildOggHeader(0x05, 60000, 68, 3), TestUtil.createByteArray(254, 254, 254), // laces TestUtil.buildTestData(3 * 254, random) ), false); assertReadGranuleOfLastPage(input, 60000); } + @Test public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException { - FakeExtractorInput input = TestData.createInput(TestUtil.buildTestData(100, random), false); + FakeExtractorInput input = OggTestData.createInput(TestUtil.buildTestData(100, random), false); try { assertReadGranuleOfLastPage(input, 60000); fail(); @@ -180,9 +199,10 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { } } + @Test public void testReadGranuleOfLastPageWithUnboundedLength() throws IOException, InterruptedException { - FakeExtractorInput input = TestData.createInput(new byte[0], true); + FakeExtractorInput input = OggTestData.createInput(new byte[0], true); try { assertReadGranuleOfLastPage(input, 60000); fail(); @@ -196,7 +216,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader(), 1, 2); while (true) { try { - assertEquals(expected, oggSeeker.readGranuleOfLastPage(input)); + assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected); break; } catch (FakeExtractorInput.SimulatedIOException e) { // ignored diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java similarity index 59% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java index 9d39eba174..c8bcffde3c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java @@ -15,67 +15,79 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; +import com.google.android.exoplayer2.testutil.OggTestData; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link OggPageHeader}. */ -public final class OggPageHeaderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class OggPageHeaderTest { + @Test public void testPopulatePageHeader() throws IOException, InterruptedException { - FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 123456, 4, 2), + FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays( + OggTestData.buildOggHeader(0x01, 123456, 4, 2), TestUtil.createByteArray(2, 2) ), true); OggPageHeader header = new OggPageHeader(); populatePageHeader(input, header, false); - assertEquals(0x01, header.type); - assertEquals(27 + 2, header.headerSize); - assertEquals(4, header.bodySize); - assertEquals(2, header.pageSegmentCount); - assertEquals(123456, header.granulePosition); - assertEquals(4, header.pageSequenceNumber); - assertEquals(0x1000, header.streamSerialNumber); - assertEquals(0x100000, header.pageChecksum); - assertEquals(0, header.revision); + assertThat(header.type).isEqualTo(0x01); + assertThat(header.headerSize).isEqualTo(27 + 2); + assertThat(header.bodySize).isEqualTo(4); + assertThat(header.pageSegmentCount).isEqualTo(2); + assertThat(header.granulePosition).isEqualTo(123456); + assertThat(header.pageSequenceNumber).isEqualTo(4); + assertThat(header.streamSerialNumber).isEqualTo(0x1000); + assertThat(header.pageChecksum).isEqualTo(0x100000); + assertThat(header.revision).isEqualTo(0); } + @Test public void testPopulatePageHeaderQuiteOnExceptionLessThan27Bytes() throws IOException, InterruptedException { - FakeExtractorInput input = TestData.createInput(TestUtil.createByteArray(2, 2), false); + FakeExtractorInput input = OggTestData.createInput(TestUtil.createByteArray(2, 2), false); OggPageHeader header = new OggPageHeader(); - assertFalse(populatePageHeader(input, header, true)); + assertThat(populatePageHeader(input, header, true)).isFalse(); } + @Test public void testPopulatePageHeaderQuiteOnExceptionNotOgg() throws IOException, InterruptedException { byte[] headerBytes = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 123456, 4, 2), + OggTestData.buildOggHeader(0x01, 123456, 4, 2), TestUtil.createByteArray(2, 2) ); // change from 'O' to 'o' headerBytes[0] = 'o'; - FakeExtractorInput input = TestData.createInput(headerBytes, false); + FakeExtractorInput input = OggTestData.createInput(headerBytes, false); OggPageHeader header = new OggPageHeader(); - assertFalse(populatePageHeader(input, header, true)); + assertThat(populatePageHeader(input, header, true)).isFalse(); } + @Test public void testPopulatePageHeaderQuiteOnExceptionWrongRevision() throws IOException, InterruptedException { byte[] headerBytes = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 123456, 4, 2), + OggTestData.buildOggHeader(0x01, 123456, 4, 2), TestUtil.createByteArray(2, 2) ); // change revision from 0 to 1 headerBytes[4] = 0x01; - FakeExtractorInput input = TestData.createInput(headerBytes, false); + FakeExtractorInput input = OggTestData.createInput(headerBytes, false); OggPageHeader header = new OggPageHeader(); - assertFalse(populatePageHeader(input, header, true)); + assertThat(populatePageHeader(input, header, true)).isFalse(); } private boolean populatePageHeader(FakeExtractorInput input, OggPageHeader header, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java new file mode 100644 index 0000000000..08b9b12a18 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.ogg; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.testutil.TestUtil; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link VorbisBitArray}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class VorbisBitArrayTest { + + @Test + public void testReadBit() { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x5c, 0x50)); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isFalse(); + } + + @Test + public void testSkipBits() { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); + bitArray.skipBits(10); + assertThat(bitArray.getPosition()).isEqualTo(10); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isFalse(); + bitArray.skipBits(1); + assertThat(bitArray.getPosition()).isEqualTo(14); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isFalse(); + } + + @Test + public void testGetPosition() throws Exception { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); + assertThat(bitArray.getPosition()).isEqualTo(0); + bitArray.readBit(); + assertThat(bitArray.getPosition()).isEqualTo(1); + bitArray.readBit(); + bitArray.readBit(); + bitArray.skipBits(4); + assertThat(bitArray.getPosition()).isEqualTo(7); + } + + @Test + public void testSetPosition() throws Exception { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); + assertThat(bitArray.getPosition()).isEqualTo(0); + bitArray.setPosition(4); + assertThat(bitArray.getPosition()).isEqualTo(4); + bitArray.setPosition(15); + assertThat(bitArray.readBit()).isFalse(); + } + + @Test + public void testReadInt32() { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F, 0xF0, 0x0F)); + assertThat(bitArray.readBits(32)).isEqualTo(0x0FF00FF0); + bitArray = new VorbisBitArray(TestUtil.createByteArray(0x0F, 0xF0, 0x0F, 0xF0)); + assertThat(bitArray.readBits(32)).isEqualTo(0xF00FF00F); + } + + @Test + public void testReadBits() throws Exception { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x03, 0x22)); + assertThat(bitArray.readBits(2)).isEqualTo(3); + bitArray.skipBits(6); + assertThat(bitArray.readBits(2)).isEqualTo(2); + bitArray.skipBits(2); + assertThat(bitArray.readBits(2)).isEqualTo(2); + bitArray.reset(); + assertThat(bitArray.readBits(16)).isEqualTo(0x2203); + } + + @Test + public void testRead4BitsBeyondBoundary() throws Exception { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x2e, 0x10)); + assertThat(bitArray.readBits(7)).isEqualTo(0x2e); + assertThat(bitArray.getPosition()).isEqualTo(7); + assertThat(bitArray.readBits(4)).isEqualTo(0x0); + } + + @Test + public void testReadBitsBeyondByteBoundaries() throws Exception { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xFF, 0x0F, 0xFF, 0x0F)); + assertThat(bitArray.readBits(32)).isEqualTo(0x0FFF0FFF); + + bitArray.reset(); + bitArray.skipBits(4); + assertThat(bitArray.readBits(16)).isEqualTo(0xF0FF); + + bitArray.reset(); + bitArray.skipBits(6); + assertThat(bitArray.readBits(12)).isEqualTo(0xc3F); + + bitArray.reset(); + bitArray.skipBits(6); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.bitsLeft()).isEqualTo(24); + + bitArray.reset(); + bitArray.skipBits(10); + assertThat(bitArray.readBits(5)).isEqualTo(3); + assertThat(bitArray.getPosition()).isEqualTo(15); + } + + @Test + public void testReadBitsIllegalLengths() throws Exception { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x03, 0x22, 0x30)); + + // reading zero bits gets 0 without advancing position + // (like a zero-bit read is defined to yield zer0) + assertThat(bitArray.readBits(0)).isEqualTo(0); + assertThat(bitArray.getPosition()).isEqualTo(0); + bitArray.readBit(); + assertThat(bitArray.getPosition()).isEqualTo(1); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java new file mode 100644 index 0000000000..20a76e83e0 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.ogg; + +import static com.google.android.exoplayer2.extractor.ogg.VorbisReader.readBits; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.ogg.VorbisReader.VorbisSetup; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; +import com.google.android.exoplayer2.testutil.OggTestData; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link VorbisReader}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class VorbisReaderTest { + + @Test + public void testReadBits() throws Exception { + assertThat(readBits((byte) 0x00, 2, 2)).isEqualTo(0); + assertThat(readBits((byte) 0x02, 1, 1)).isEqualTo(1); + assertThat(readBits((byte) 0xF0, 4, 4)).isEqualTo(15); + assertThat(readBits((byte) 0x80, 1, 7)).isEqualTo(1); + } + + @Test + public void testAppendNumberOfSamples() throws Exception { + ParsableByteArray buffer = new ParsableByteArray(4); + buffer.setLimit(0); + VorbisReader.appendNumberOfSamples(buffer, 0x01234567); + assertThat(buffer.limit()).isEqualTo(4); + assertThat(buffer.data[0]).isEqualTo(0x67); + assertThat(buffer.data[1]).isEqualTo(0x45); + assertThat(buffer.data[2]).isEqualTo(0x23); + assertThat(buffer.data[3]).isEqualTo(0x01); + } + + @Test + public void testReadSetupHeadersWithIOExceptions() throws IOException, InterruptedException { + byte[] data = OggTestData.getVorbisHeaderPages(); + ExtractorInput input = new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true) + .setSimulateUnknownLength(true).setSimulatePartialReads(true).build(); + + VorbisReader reader = new VorbisReader(); + VorbisReader.VorbisSetup vorbisSetup = readSetupHeaders(reader, input); + + assertThat(vorbisSetup.idHeader).isNotNull(); + assertThat(vorbisSetup.commentHeader).isNotNull(); + assertThat(vorbisSetup.setupHeaderData).isNotNull(); + assertThat(vorbisSetup.modes).isNotNull(); + + assertThat(vorbisSetup.commentHeader.length).isEqualTo(45); + assertThat(vorbisSetup.idHeader.data).hasLength(30); + assertThat(vorbisSetup.setupHeaderData).hasLength(3597); + + assertThat(vorbisSetup.idHeader.bitrateMax).isEqualTo(-1); + assertThat(vorbisSetup.idHeader.bitrateMin).isEqualTo(-1); + assertThat(vorbisSetup.idHeader.bitrateNominal).isEqualTo(66666); + assertThat(vorbisSetup.idHeader.blockSize0).isEqualTo(512); + assertThat(vorbisSetup.idHeader.blockSize1).isEqualTo(1024); + assertThat(vorbisSetup.idHeader.channels).isEqualTo(2); + assertThat(vorbisSetup.idHeader.framingFlag).isTrue(); + assertThat(vorbisSetup.idHeader.sampleRate).isEqualTo(22050); + assertThat(vorbisSetup.idHeader.version).isEqualTo(0); + + assertThat(vorbisSetup.commentHeader.vendor).isEqualTo("Xiph.Org libVorbis I 20030909"); + assertThat(vorbisSetup.iLogModes).isEqualTo(1); + + assertThat(vorbisSetup.setupHeaderData[vorbisSetup.setupHeaderData.length - 1]) + .isEqualTo(data[data.length - 1]); + + assertThat(vorbisSetup.modes[0].blockFlag).isFalse(); + assertThat(vorbisSetup.modes[1].blockFlag).isTrue(); + } + + private static VorbisSetup readSetupHeaders(VorbisReader reader, ExtractorInput input) + throws IOException, InterruptedException { + OggPacket oggPacket = new OggPacket(); + while (true) { + try { + if (!oggPacket.populate(input)) { + fail(); + } + VorbisSetup vorbisSetup = reader.readSetupHeaders(oggPacket.getPayload()); + if (vorbisSetup != null) { + return vorbisSetup; + } + } catch (SimulatedIOException e) { + // Ignore. + } + } + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java new file mode 100644 index 0000000000..bdc573f218 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.ogg; + +import static com.google.android.exoplayer2.extractor.ogg.VorbisUtil.iLog; +import static com.google.android.exoplayer2.extractor.ogg.VorbisUtil.verifyVorbisHeaderCapturePattern; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.testutil.OggTestData; +import com.google.android.exoplayer2.util.ParsableByteArray; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link VorbisUtil}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class VorbisUtilTest { + + @Test + public void testILog() throws Exception { + assertThat(iLog(0)).isEqualTo(0); + assertThat(iLog(1)).isEqualTo(1); + assertThat(iLog(2)).isEqualTo(2); + assertThat(iLog(3)).isEqualTo(2); + assertThat(iLog(4)).isEqualTo(3); + assertThat(iLog(5)).isEqualTo(3); + assertThat(iLog(8)).isEqualTo(4); + assertThat(iLog(-1)).isEqualTo(0); + assertThat(iLog(-122)).isEqualTo(0); + } + + @Test + public void testReadIdHeader() throws Exception { + byte[] data = OggTestData.getIdentificationHeaderData(); + ParsableByteArray headerData = new ParsableByteArray(data, data.length); + VorbisUtil.VorbisIdHeader vorbisIdHeader = + VorbisUtil.readVorbisIdentificationHeader(headerData); + + assertThat(vorbisIdHeader.sampleRate).isEqualTo(22050); + assertThat(vorbisIdHeader.version).isEqualTo(0); + assertThat(vorbisIdHeader.framingFlag).isTrue(); + assertThat(vorbisIdHeader.channels).isEqualTo(2); + assertThat(vorbisIdHeader.blockSize0).isEqualTo(512); + assertThat(vorbisIdHeader.blockSize1).isEqualTo(1024); + assertThat(vorbisIdHeader.bitrateMax).isEqualTo(-1); + assertThat(vorbisIdHeader.bitrateMin).isEqualTo(-1); + assertThat(vorbisIdHeader.bitrateNominal).isEqualTo(66666); + assertThat(vorbisIdHeader.getApproximateBitrate()).isEqualTo(66666); + } + + @Test + public void testReadCommentHeader() throws ParserException { + byte[] data = OggTestData.getCommentHeaderDataUTF8(); + ParsableByteArray headerData = new ParsableByteArray(data, data.length); + VorbisUtil.CommentHeader commentHeader = VorbisUtil.readVorbisCommentHeader(headerData); + + assertThat(commentHeader.vendor).isEqualTo("Xiph.Org libVorbis I 20120203 (Omnipresent)"); + assertThat(commentHeader.comments).hasLength(3); + assertThat(commentHeader.comments[0]).isEqualTo("ALBUM=äö"); + assertThat(commentHeader.comments[1]).isEqualTo("TITLE=A sample song"); + assertThat(commentHeader.comments[2]).isEqualTo("ARTIST=Google"); + } + + @Test + public void testReadVorbisModes() throws ParserException { + byte[] data = OggTestData.getSetupHeaderData(); + ParsableByteArray headerData = new ParsableByteArray(data, data.length); + VorbisUtil.Mode[] modes = VorbisUtil.readVorbisModes(headerData, 2); + + assertThat(modes).hasLength(2); + assertThat(modes[0].blockFlag).isFalse(); + assertThat(modes[0].mapping).isEqualTo(0); + assertThat(modes[0].transformType).isEqualTo(0); + assertThat(modes[0].windowType).isEqualTo(0); + assertThat(modes[1].blockFlag).isTrue(); + assertThat(modes[1].mapping).isEqualTo(1); + assertThat(modes[1].transformType).isEqualTo(0); + assertThat(modes[1].windowType).isEqualTo(0); + } + + @Test + public void testVerifyVorbisHeaderCapturePattern() throws ParserException { + ParsableByteArray header = new ParsableByteArray( + new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); + assertThat(verifyVorbisHeaderCapturePattern(0x01, header, false)).isTrue(); + } + + @Test + public void testVerifyVorbisHeaderCapturePatternInvalidHeader() { + ParsableByteArray header = new ParsableByteArray( + new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); + try { + VorbisUtil.verifyVorbisHeaderCapturePattern(0x99, header, false); + fail(); + } catch (ParserException e) { + assertThat(e.getMessage()).isEqualTo("expected header type 99"); + } + } + + @Test + public void testVerifyVorbisHeaderCapturePatternInvalidHeaderQuite() throws ParserException { + ParsableByteArray header = new ParsableByteArray( + new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); + assertThat(verifyVorbisHeaderCapturePattern(0x99, header, true)).isFalse(); + } + + @Test + public void testVerifyVorbisHeaderCapturePatternInvalidPattern() { + ParsableByteArray header = new ParsableByteArray( + new byte[] {0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'}); + try { + VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, false); + fail(); + } catch (ParserException e) { + assertThat(e.getMessage()).isEqualTo("expected characters 'vorbis'"); + } + } + + @Test + public void testVerifyVorbisHeaderCapturePatternQuiteInvalidPatternQuite() + throws ParserException { + ParsableByteArray header = new ParsableByteArray( + new byte[] {0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'}); + assertThat(verifyVorbisHeaderCapturePattern(0x01, header, true)).isFalse(); + } + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java similarity index 81% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java index c4d9de3100..56668d5124 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java @@ -15,26 +15,35 @@ */ package com.google.android.exoplayer2.extractor.ts; +import static com.google.common.truth.Truth.assertThat; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; + import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import junit.framework.TestCase; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link SectionReader}. */ -public class SectionReaderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class SectionReaderTest { private byte[] packetPayload; private CustomSectionPayloadReader payloadReader; private SectionReader reader; - @Override + @Before public void setUp() { packetPayload = new byte[512]; Arrays.fill(packetPayload, (byte) 0xFF); @@ -44,27 +53,30 @@ public class SectionReaderTest extends TestCase { new TsPayloadReader.TrackIdGenerator(0, 1)); } + @Test public void testSingleOnePacketSection() { packetPayload[0] = 3; insertTableSection(4, (byte) 99, 3); reader.consume(new ParsableByteArray(packetPayload), true); - assertEquals(Collections.singletonList(99), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(99)); } + @Test public void testHeaderSplitAcrossPackets() { packetPayload[0] = 3; // The first packet includes a pointer_field. insertTableSection(4, (byte) 100, 3); // This section header spreads across both packets. ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 5); reader.consume(firstPacket, true); - assertEquals(Collections.emptyList(), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEmpty(); ParsableByteArray secondPacket = new ParsableByteArray(packetPayload); secondPacket.setPosition(5); reader.consume(secondPacket, false); - assertEquals(Collections.singletonList(100), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(100)); } + @Test public void testFiveSectionsInTwoPackets() { packetPayload[0] = 0; // The first packet includes a pointer_field. insertTableSection(1, (byte) 101, 10); @@ -76,14 +88,15 @@ public class SectionReaderTest extends TestCase { ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 40); reader.consume(firstPacket, true); - assertEquals(Arrays.asList(101, 102, 103), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEqualTo(asList(101, 102, 103)); ParsableByteArray secondPacket = new ParsableByteArray(packetPayload); secondPacket.setPosition(40); reader.consume(secondPacket, true); - assertEquals(Arrays.asList(101, 102, 103, 104, 105), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEqualTo(asList(101, 102, 103, 104, 105)); } + @Test public void testLongSectionAcrossFourPackets() { packetPayload[0] = 13; // The first packet includes a pointer_field. insertTableSection(1, (byte) 106, 10); // First section. Should be skipped. @@ -95,24 +108,25 @@ public class SectionReaderTest extends TestCase { ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 100); reader.consume(firstPacket, true); - assertEquals(Collections.emptyList(), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEmpty(); ParsableByteArray secondPacket = new ParsableByteArray(packetPayload, 200); secondPacket.setPosition(100); reader.consume(secondPacket, false); - assertEquals(Collections.emptyList(), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEmpty(); ParsableByteArray thirdPacket = new ParsableByteArray(packetPayload, 300); thirdPacket.setPosition(200); reader.consume(thirdPacket, false); - assertEquals(Collections.emptyList(), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEmpty(); ParsableByteArray fourthPacket = new ParsableByteArray(packetPayload); fourthPacket.setPosition(300); reader.consume(fourthPacket, true); - assertEquals(Arrays.asList(107, 108), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEqualTo(asList(107, 108)); } + @Test public void testSeek() { packetPayload[0] = 13; // The first packet includes a pointer_field. insertTableSection(1, (byte) 109, 10); // First section. Should be skipped. @@ -124,26 +138,27 @@ public class SectionReaderTest extends TestCase { ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 100); reader.consume(firstPacket, true); - assertEquals(Collections.emptyList(), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEmpty(); ParsableByteArray secondPacket = new ParsableByteArray(packetPayload, 200); secondPacket.setPosition(100); reader.consume(secondPacket, false); - assertEquals(Collections.emptyList(), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEmpty(); ParsableByteArray thirdPacket = new ParsableByteArray(packetPayload, 300); thirdPacket.setPosition(200); reader.consume(thirdPacket, false); - assertEquals(Collections.emptyList(), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEmpty(); reader.seek(); ParsableByteArray fourthPacket = new ParsableByteArray(packetPayload); fourthPacket.setPosition(300); reader.consume(fourthPacket, true); - assertEquals(Collections.singletonList(111), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(111)); } + @Test public void testCrcChecks() { byte[] correctCrcPat = new byte[] { (byte) 0x0, (byte) 0x0, (byte) 0xb0, (byte) 0xd, (byte) 0x0, (byte) 0x1, (byte) 0xc1, @@ -153,9 +168,9 @@ public class SectionReaderTest extends TestCase { // Crc field is incorrect, and should not be passed to the payload reader. incorrectCrcPat[16]--; reader.consume(new ParsableByteArray(correctCrcPat), true); - assertEquals(Collections.singletonList(0), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(0)); reader.consume(new ParsableByteArray(incorrectCrcPat), true); - assertEquals(Collections.singletonList(0), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(0)); } // Internal methods. diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java similarity index 70% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java index b33dfd1067..1ce0ccb93d 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java @@ -15,17 +15,24 @@ */ package com.google.android.exoplayer2.metadata.emsg; -import android.test.MoreAsserts; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import java.nio.ByteBuffer; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link EventMessageDecoder}. */ -public final class EventMessageDecoderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class EventMessageDecoderTest { + @Test public void testDecodeEventMessage() { byte[] rawEmsgBody = new byte[] { 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" @@ -39,13 +46,13 @@ public final class EventMessageDecoderTest extends TestCase { MetadataInputBuffer buffer = new MetadataInputBuffer(); buffer.data = ByteBuffer.allocate(rawEmsgBody.length).put(rawEmsgBody); Metadata metadata = decoder.decode(buffer); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); EventMessage eventMessage = (EventMessage) metadata.get(0); - assertEquals("urn:test", eventMessage.schemeIdUri); - assertEquals("123", eventMessage.value); - assertEquals(3000, eventMessage.durationMs); - assertEquals(1000403, eventMessage.id); - MoreAsserts.assertEquals(new byte[] {0, 1, 2, 3, 4}, eventMessage.messageData); + assertThat(eventMessage.schemeIdUri).isEqualTo("urn:test"); + assertThat(eventMessage.value).isEqualTo("123"); + assertThat(eventMessage.durationMs).isEqualTo(3000); + assertThat(eventMessage.id).isEqualTo(1000403); + assertThat(eventMessage.messageData).isEqualTo(new byte[]{0, 1, 2, 3, 4}); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java similarity index 74% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java index baafb6b18b..b48a071d0d 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java @@ -15,14 +15,22 @@ */ package com.google.android.exoplayer2.metadata.emsg; +import static com.google.common.truth.Truth.assertThat; + import android.os.Parcel; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link EventMessage}. */ -public final class EventMessageTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class EventMessageTest { + @Test public void testEventMessageParcelable() { EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403, new byte[] {0, 1, 2, 3, 4}); @@ -33,7 +41,7 @@ public final class EventMessageTest extends TestCase { parcel.setDataPosition(0); EventMessage fromParcelEventMessage = EventMessage.CREATOR.createFromParcel(parcel); // Assert equals. - assertEquals(eventMessage, fromParcelEventMessage); + assertThat(fromParcelEventMessage).isEqualTo(eventMessage); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java similarity index 75% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java index 182ae6f1c9..a42b71731a 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java @@ -15,14 +15,22 @@ */ package com.google.android.exoplayer2.metadata.id3; +import static com.google.common.truth.Truth.assertThat; + import android.os.Parcel; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link ChapterFrame}. */ -public final class ChapterFrameTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ChapterFrameTest { + @Test public void testParcelable() { Id3Frame[] subFrames = new Id3Frame[] { new TextInformationFrame("TIT2", null, "title"), @@ -35,7 +43,7 @@ public final class ChapterFrameTest extends TestCase { parcel.setDataPosition(0); ChapterFrame chapterFrameFromParcel = ChapterFrame.CREATOR.createFromParcel(parcel); - assertEquals(chapterFrameToParcel, chapterFrameFromParcel); + assertThat(chapterFrameFromParcel).isEqualTo(chapterFrameToParcel); parcel.recycle(); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java similarity index 76% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java index 9641de7669..9636b04e51 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java @@ -15,14 +15,22 @@ */ package com.google.android.exoplayer2.metadata.id3; +import static com.google.common.truth.Truth.assertThat; + import android.os.Parcel; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link ChapterTocFrame}. */ -public final class ChapterTocFrameTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ChapterTocFrameTest { + @Test public void testParcelable() { String[] children = new String[] {"child0", "child1"}; Id3Frame[] subFrames = new Id3Frame[] { @@ -37,7 +45,7 @@ public final class ChapterTocFrameTest extends TestCase { parcel.setDataPosition(0); ChapterTocFrame chapterTocFrameFromParcel = ChapterTocFrame.CREATOR.createFromParcel(parcel); - assertEquals(chapterTocFrameToParcel, chapterTocFrameFromParcel); + assertThat(chapterTocFrameFromParcel).isEqualTo(chapterTocFrameToParcel); parcel.recycle(); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java similarity index 66% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java index 6b39ed1645..06ce330146 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java @@ -15,182 +15,197 @@ */ package com.google.android.exoplayer2.metadata.id3; -import android.test.MoreAsserts; +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.util.Assertions; -import junit.framework.TestCase; +import java.nio.charset.Charset; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link Id3Decoder}. */ -public final class Id3DecoderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class Id3DecoderTest { private static final byte[] TAG_HEADER = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 0}; private static final int FRAME_HEADER_LENGTH = 10; private static final int ID3_TEXT_ENCODING_UTF_8 = 3; + @Test public void testDecodeTxxxFrame() throws MetadataDecoderException { byte[] rawId3 = buildSingleFrameTag("TXXX", new byte[] {3, 0, 109, 100, 105, 97, 108, 111, 103, 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50, 55, 54, 54, 52, 95, 115, 116, 97, 114, 116, 0}); Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); - assertEquals("TXXX", textInformationFrame.id); - assertEquals("", textInformationFrame.description); - assertEquals("mdialog_VINDICO1527664_start", textInformationFrame.value); + assertThat(textInformationFrame.id).isEqualTo("TXXX"); + assertThat(textInformationFrame.description).isEmpty(); + assertThat(textInformationFrame.value).isEqualTo("mdialog_VINDICO1527664_start"); // Test empty. rawId3 = buildSingleFrameTag("TXXX", new byte[0]); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(0, metadata.length()); + assertThat(metadata.length()).isEqualTo(0); // Test encoding byte only. rawId3 = buildSingleFrameTag("TXXX", new byte[] {ID3_TEXT_ENCODING_UTF_8}); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); textInformationFrame = (TextInformationFrame) metadata.get(0); - assertEquals("TXXX", textInformationFrame.id); - assertEquals("", textInformationFrame.description); - assertEquals("", textInformationFrame.value); + assertThat(textInformationFrame.id).isEqualTo("TXXX"); + assertThat(textInformationFrame.description).isEmpty(); + assertThat(textInformationFrame.value).isEmpty(); } + @Test public void testDecodeTextInformationFrame() throws MetadataDecoderException { byte[] rawId3 = buildSingleFrameTag("TIT2", new byte[] {3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0}); Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); - assertEquals("TIT2", textInformationFrame.id); - assertNull(textInformationFrame.description); - assertEquals("Hello World", textInformationFrame.value); + assertThat(textInformationFrame.id).isEqualTo("TIT2"); + assertThat(textInformationFrame.description).isNull(); + assertThat(textInformationFrame.value).isEqualTo("Hello World"); // Test empty. rawId3 = buildSingleFrameTag("TIT2", new byte[0]); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(0, metadata.length()); + assertThat(metadata.length()).isEqualTo(0); // Test encoding byte only. rawId3 = buildSingleFrameTag("TIT2", new byte[] {ID3_TEXT_ENCODING_UTF_8}); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); textInformationFrame = (TextInformationFrame) metadata.get(0); - assertEquals("TIT2", textInformationFrame.id); - assertEquals(null, textInformationFrame.description); - assertEquals("", textInformationFrame.value); + assertThat(textInformationFrame.id).isEqualTo("TIT2"); + assertThat(textInformationFrame.description).isNull(); + assertThat(textInformationFrame.value).isEmpty(); } + @Test public void testDecodeWxxxFrame() throws MetadataDecoderException { byte[] rawId3 = buildSingleFrameTag("WXXX", new byte[] {ID3_TEXT_ENCODING_UTF_8, 116, 101, 115, 116, 0, 104, 116, 116, 112, 115, 58, 47, 47, 116, 101, 115, 116, 46, 99, 111, 109, 47, 97, 98, 99, 63, 100, 101, 102}); Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); UrlLinkFrame urlLinkFrame = (UrlLinkFrame) metadata.get(0); - assertEquals("WXXX", urlLinkFrame.id); - assertEquals("test", urlLinkFrame.description); - assertEquals("https://test.com/abc?def", urlLinkFrame.url); + assertThat(urlLinkFrame.id).isEqualTo("WXXX"); + assertThat(urlLinkFrame.description).isEqualTo("test"); + assertThat(urlLinkFrame.url).isEqualTo("https://test.com/abc?def"); // Test empty. rawId3 = buildSingleFrameTag("WXXX", new byte[0]); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(0, metadata.length()); + assertThat(metadata.length()).isEqualTo(0); // Test encoding byte only. rawId3 = buildSingleFrameTag("WXXX", new byte[] {ID3_TEXT_ENCODING_UTF_8}); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); urlLinkFrame = (UrlLinkFrame) metadata.get(0); - assertEquals("WXXX", urlLinkFrame.id); - assertEquals("", urlLinkFrame.description); - assertEquals("", urlLinkFrame.url); + assertThat(urlLinkFrame.id).isEqualTo("WXXX"); + assertThat(urlLinkFrame.description).isEmpty(); + assertThat(urlLinkFrame.url).isEmpty(); } + @Test public void testDecodeUrlLinkFrame() throws MetadataDecoderException { byte[] rawId3 = buildSingleFrameTag("WCOM", new byte[] {104, 116, 116, 112, 115, 58, 47, 47, 116, 101, 115, 116, 46, 99, 111, 109, 47, 97, 98, 99, 63, 100, 101, 102}); Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); UrlLinkFrame urlLinkFrame = (UrlLinkFrame) metadata.get(0); - assertEquals("WCOM", urlLinkFrame.id); - assertEquals(null, urlLinkFrame.description); - assertEquals("https://test.com/abc?def", urlLinkFrame.url); + assertThat(urlLinkFrame.id).isEqualTo("WCOM"); + assertThat(urlLinkFrame.description).isNull(); + assertThat(urlLinkFrame.url).isEqualTo("https://test.com/abc?def"); // Test empty. rawId3 = buildSingleFrameTag("WCOM", new byte[0]); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); urlLinkFrame = (UrlLinkFrame) metadata.get(0); - assertEquals("WCOM", urlLinkFrame.id); - assertEquals(null, urlLinkFrame.description); - assertEquals("", urlLinkFrame.url); + assertThat(urlLinkFrame.id).isEqualTo("WCOM"); + assertThat(urlLinkFrame.description).isNull(); + assertThat(urlLinkFrame.url).isEmpty(); } + @Test public void testDecodePrivFrame() throws MetadataDecoderException { byte[] rawId3 = buildSingleFrameTag("PRIV", new byte[] {116, 101, 115, 116, 0, 1, 2, 3, 4}); Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); PrivFrame privFrame = (PrivFrame) metadata.get(0); - assertEquals("test", privFrame.owner); - MoreAsserts.assertEquals(new byte[] {1, 2, 3, 4}, privFrame.privateData); + assertThat(privFrame.owner).isEqualTo("test"); + assertThat(privFrame.privateData).isEqualTo(new byte[]{1, 2, 3, 4}); // Test empty. rawId3 = buildSingleFrameTag("PRIV", new byte[0]); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); privFrame = (PrivFrame) metadata.get(0); - assertEquals("", privFrame.owner); - MoreAsserts.assertEquals(new byte[0], privFrame.privateData); + assertThat(privFrame.owner).isEmpty(); + assertThat(privFrame.privateData).isEqualTo(new byte[0]); } + @Test public void testDecodeApicFrame() throws MetadataDecoderException { byte[] rawId3 = buildSingleFrameTag("APIC", new byte[] {3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0}); Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); ApicFrame apicFrame = (ApicFrame) metadata.get(0); - assertEquals("image/jpeg", apicFrame.mimeType); - assertEquals(16, apicFrame.pictureType); - assertEquals("Hello World", apicFrame.description); - assertEquals(10, apicFrame.pictureData.length); - MoreAsserts.assertEquals(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, apicFrame.pictureData); + assertThat(apicFrame.mimeType).isEqualTo("image/jpeg"); + assertThat(apicFrame.pictureType).isEqualTo(16); + assertThat(apicFrame.description).isEqualTo("Hello World"); + assertThat(apicFrame.pictureData).hasLength(10); + assertThat(apicFrame.pictureData).isEqualTo(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}); } + @Test public void testDecodeCommentFrame() throws MetadataDecoderException { byte[] rawId3 = buildSingleFrameTag("COMM", new byte[] {ID3_TEXT_ENCODING_UTF_8, 101, 110, 103, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 0, 116, 101, 120, 116, 0}); Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); CommentFrame commentFrame = (CommentFrame) metadata.get(0); - assertEquals("eng", commentFrame.language); - assertEquals("description", commentFrame.description); - assertEquals("text", commentFrame.text); + assertThat(commentFrame.language).isEqualTo("eng"); + assertThat(commentFrame.description).isEqualTo("description"); + assertThat(commentFrame.text).isEqualTo("text"); // Test empty. rawId3 = buildSingleFrameTag("COMM", new byte[0]); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(0, metadata.length()); + assertThat(metadata.length()).isEqualTo(0); // Test language only. rawId3 = buildSingleFrameTag("COMM", new byte[] {ID3_TEXT_ENCODING_UTF_8, 101, 110, 103}); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); commentFrame = (CommentFrame) metadata.get(0); - assertEquals("eng", commentFrame.language); - assertEquals("", commentFrame.description); - assertEquals("", commentFrame.text); + assertThat(commentFrame.language).isEqualTo("eng"); + assertThat(commentFrame.description).isEmpty(); + assertThat(commentFrame.text).isEmpty(); } private static byte[] buildSingleFrameTag(String frameId, byte[] frameData) { - byte[] frameIdBytes = frameId.getBytes(); + byte[] frameIdBytes = frameId.getBytes(Charset.forName(C.UTF8_NAME)); Assertions.checkState(frameIdBytes.length == 4); byte[] tagData = new byte[TAG_HEADER.length + FRAME_HEADER_LENGTH + frameData.length]; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java similarity index 73% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java index c50ff06699..15cb9b23c5 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java @@ -15,29 +15,38 @@ */ package com.google.android.exoplayer2.metadata.scte35; -import com.google.android.exoplayer2.C; +import static com.google.android.exoplayer2.C.TIME_UNSET; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import com.google.android.exoplayer2.util.TimestampAdjuster; import java.nio.ByteBuffer; import java.util.List; -import junit.framework.TestCase; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link SpliceInfoDecoder}. */ -public final class SpliceInfoDecoderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class SpliceInfoDecoderTest { private SpliceInfoDecoder decoder; private MetadataInputBuffer inputBuffer; - @Override + @Before public void setUp() { decoder = new SpliceInfoDecoder(); inputBuffer = new MetadataInputBuffer(); } + @Test public void testWrappedAroundTimeSignalCommand() throws MetadataDecoderException { byte[] rawTimeSignalSection = new byte[] { 0, // table_id. @@ -59,11 +68,12 @@ public final class SpliceInfoDecoderTest extends TestCase { // The playback position is 57:15:58.43 approximately. // With this offset, the playback position pts before wrapping is 0x451ebf851. Metadata metadata = feedInputBuffer(rawTimeSignalSection, 0x3000000000L, -0x50000L); - assertEquals(1, metadata.length()); - assertEquals(removePtsConversionPrecisionError(0x3001000000L, inputBuffer.subsampleOffsetUs), - ((TimeSignalCommand) metadata.get(0)).playbackPositionUs); + assertThat(metadata.length()).isEqualTo(1); + assertThat(((TimeSignalCommand) metadata.get(0)).playbackPositionUs) + .isEqualTo(removePtsConversionPrecisionError(0x3001000000L, inputBuffer.subsampleOffsetUs)); } + @Test public void test2SpliceInsertCommands() throws MetadataDecoderException { byte[] rawSpliceInsertCommand1 = new byte[] { 0, // table_id. @@ -91,18 +101,18 @@ public final class SpliceInfoDecoderTest extends TestCase { 0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction). Metadata metadata = feedInputBuffer(rawSpliceInsertCommand1, 2000000, 3000000); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); SpliceInsertCommand command = (SpliceInsertCommand) metadata.get(0); - assertEquals(66, command.spliceEventId); - assertFalse(command.spliceEventCancelIndicator); - assertFalse(command.outOfNetworkIndicator); - assertTrue(command.programSpliceFlag); - assertFalse(command.spliceImmediateFlag); - assertEquals(3000000, command.programSplicePlaybackPositionUs); - assertEquals(C.TIME_UNSET, command.breakDuration); - assertEquals(16, command.uniqueProgramId); - assertEquals(1, command.availNum); - assertEquals(2, command.availsExpected); + assertThat(command.spliceEventId).isEqualTo(66); + assertThat(command.spliceEventCancelIndicator).isFalse(); + assertThat(command.outOfNetworkIndicator).isFalse(); + assertThat(command.programSpliceFlag).isTrue(); + assertThat(command.spliceImmediateFlag).isFalse(); + assertThat(command.programSplicePlaybackPositionUs).isEqualTo(3000000); + assertThat(command.breakDuration).isEqualTo(TIME_UNSET); + assertThat(command.uniqueProgramId).isEqualTo(16); + assertThat(command.availNum).isEqualTo(1); + assertThat(command.availsExpected).isEqualTo(2); byte[] rawSpliceInsertCommand2 = new byte[] { 0, // table_id. @@ -137,24 +147,24 @@ public final class SpliceInfoDecoderTest extends TestCase { // By changing the subsample offset we force adjuster reconstruction. long subsampleOffset = 1000011; metadata = feedInputBuffer(rawSpliceInsertCommand2, 1000000, subsampleOffset); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); command = (SpliceInsertCommand) metadata.get(0); - assertEquals(0xffffffffL, command.spliceEventId); - assertFalse(command.spliceEventCancelIndicator); - assertFalse(command.outOfNetworkIndicator); - assertFalse(command.programSpliceFlag); - assertFalse(command.spliceImmediateFlag); - assertEquals(C.TIME_UNSET, command.programSplicePlaybackPositionUs); - assertEquals(C.TIME_UNSET, command.breakDuration); + assertThat(command.spliceEventId).isEqualTo(0xffffffffL); + assertThat(command.spliceEventCancelIndicator).isFalse(); + assertThat(command.outOfNetworkIndicator).isFalse(); + assertThat(command.programSpliceFlag).isFalse(); + assertThat(command.spliceImmediateFlag).isFalse(); + assertThat(command.programSplicePlaybackPositionUs).isEqualTo(TIME_UNSET); + assertThat(command.breakDuration).isEqualTo(TIME_UNSET); List componentSplices = command.componentSpliceList; - assertEquals(2, componentSplices.size()); - assertEquals(16, componentSplices.get(0).componentTag); - assertEquals(1000000, componentSplices.get(0).componentSplicePlaybackPositionUs); - assertEquals(17, componentSplices.get(1).componentTag); - assertEquals(C.TIME_UNSET, componentSplices.get(1).componentSplicePts); - assertEquals(32, command.uniqueProgramId); - assertEquals(1, command.availNum); - assertEquals(2, command.availsExpected); + assertThat(componentSplices).hasSize(2); + assertThat(componentSplices.get(0).componentTag).isEqualTo(16); + assertThat(componentSplices.get(0).componentSplicePlaybackPositionUs).isEqualTo(1000000); + assertThat(componentSplices.get(1).componentTag).isEqualTo(17); + assertThat(componentSplices.get(1).componentSplicePts).isEqualTo(TIME_UNSET); + assertThat(command.uniqueProgramId).isEqualTo(32); + assertThat(command.availNum).isEqualTo(1); + assertThat(command.availsExpected).isEqualTo(2); } private Metadata feedInputBuffer(byte[] data, long timeUs, long subsampleOffset) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java new file mode 100644 index 0000000000..95dd218092 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.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.offline; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.util.ClosedSource; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit tests for {@link ProgressiveDownloadAction}. + */ +@ClosedSource(reason = "Not ready yet") +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class ProgressiveDownloadActionTest { + + @Test + public void testDownloadActionIsNotRemoveAction() throws Exception { + ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); + assertThat(action.isRemoveAction()).isFalse(); + } + + @Test + public void testRemoveActionIsRemoveAction() throws Exception { + ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true); + assertThat(action2.isRemoveAction()).isTrue(); + } + + @Test + public void testCreateDownloader() throws Exception { + MockitoAnnotations.initMocks(this); + ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); + DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper( + Mockito.mock(Cache.class), DummyDataSource.FACTORY); + assertThat(action.createDownloader(constructorHelper)).isNotNull(); + } + + @Test + public void testSameUriCacheKeyDifferentAction_IsSameMedia() throws Exception { + ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true); + ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, false); + assertThat(action1.isSameMedia(action2)).isTrue(); + } + + @Test + public void testNullCacheKeyDifferentUriAction_IsNotSameMedia() throws Exception { + ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri2", null, true); + ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, false); + assertThat(action3.isSameMedia(action4)).isFalse(); + } + + @Test + public void testSameCacheKeyDifferentUriAction_IsSameMedia() throws Exception { + ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri2", "key", true); + ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", "key", false); + assertThat(action5.isSameMedia(action6)).isTrue(); + } + + @Test + public void testSameUriDifferentCacheKeyAction_IsNotSameMedia() throws Exception { + ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true); + ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", false); + assertThat(action7.isSameMedia(action8)).isFalse(); + } + + @Test + public void testEquals() throws Exception { + ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true); + assertThat(action1.equals(action1)).isTrue(); + + ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true); + ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri", null, true); + assertThat(action2.equals(action3)).isTrue(); + + ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, true); + ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri", null, false); + assertThat(action4.equals(action5)).isFalse(); + + ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", null, true); + ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true); + assertThat(action6.equals(action7)).isFalse(); + + ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", true); + ProgressiveDownloadAction action9 = new ProgressiveDownloadAction("uri", "key", true); + assertThat(action8.equals(action9)).isFalse(); + + ProgressiveDownloadAction action10 = new ProgressiveDownloadAction("uri", null, true); + ProgressiveDownloadAction action11 = new ProgressiveDownloadAction("uri2", null, true); + assertThat(action10.equals(action11)).isFalse(); + } + + @Test + public void testSerializerGetType() throws Exception { + ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); + assertThat(action.getType()).isNotNull(); + } + + @Test + public void testSerializerWriteRead() throws Exception { + doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri1", null, false)); + doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri2", "key", true)); + } + + private static void doTestSerializationRoundTrip(ProgressiveDownloadAction action1) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(out); + action1.writeToStream(output); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + DataInputStream input = new DataInputStream(in); + DownloadAction action2 = ProgressiveDownloadAction.DESERIALIZER.readFromStream(input); + + assertThat(action2).isEqualTo(action1); + } + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java similarity index 86% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 77e61e39a9..49983fae30 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -15,7 +15,14 @@ */ package com.google.android.exoplayer2.source; -import android.test.MoreAsserts; +import static com.google.android.exoplayer2.C.RESULT_BUFFER_READ; +import static com.google.android.exoplayer2.C.RESULT_FORMAT_READ; +import static com.google.android.exoplayer2.C.RESULT_NOTHING_READ; +import static com.google.android.exoplayer2.source.SampleQueue.ADVANCE_FAILED; +import static com.google.common.truth.Truth.assertThat; +import static java.lang.Long.MIN_VALUE; +import static java.util.Arrays.copyOfRange; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; @@ -24,13 +31,19 @@ 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; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link SampleQueue}. */ -public class SampleQueueTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class SampleQueueTest { private static final int ALLOCATION_SIZE = 16; @@ -75,24 +88,23 @@ public class SampleQueueTest extends TestCase { private FormatHolder formatHolder; private DecoderInputBuffer inputBuffer; - @Override + @Before public void setUp() throws Exception { - super.setUp(); allocator = new DefaultAllocator(false, ALLOCATION_SIZE); sampleQueue = new SampleQueue(allocator); formatHolder = new FormatHolder(); inputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); } - @Override + @After public void tearDown() throws Exception { - super.tearDown(); allocator = null; sampleQueue = null; formatHolder = null; inputBuffer = null; } + @Test public void testResetReleasesAllocations() { writeTestData(); assertAllocationCount(10); @@ -100,10 +112,12 @@ public class SampleQueueTest extends TestCase { assertAllocationCount(0); } + @Test public void testReadWithoutWrite() { assertNoSamplesToRead(null); } + @Test public void testReadFormatDeduplicated() { sampleQueue.format(TEST_FORMAT_1); assertReadFormat(false, TEST_FORMAT_1); @@ -115,6 +129,7 @@ public class SampleQueueTest extends TestCase { assertNoSamplesToRead(TEST_FORMAT_1); } + @Test public void testReadSingleSamples() { sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE); @@ -173,9 +188,10 @@ public class SampleQueueTest extends TestCase { assertAllocationCount(0); } + @Test public void testReadMultiSamples() { writeTestData(); - assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP); assertAllocationCount(10); assertReadTestData(); assertAllocationCount(10); @@ -183,6 +199,7 @@ public class SampleQueueTest extends TestCase { assertAllocationCount(0); } + @Test public void testReadMultiSamplesTwice() { writeTestData(); writeTestData(); @@ -194,19 +211,21 @@ public class SampleQueueTest extends TestCase { assertAllocationCount(0); } + @Test public void testReadMultiWithRewind() { writeTestData(); assertReadTestData(); - assertEquals(8, sampleQueue.getReadIndex()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(8); assertAllocationCount(10); // Rewind. sampleQueue.rewind(); assertAllocationCount(10); // Read again. - assertEquals(0, sampleQueue.getReadIndex()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(0); assertReadTestData(); } + @Test public void testRewindAfterDiscard() { writeTestData(); assertReadTestData(); @@ -216,10 +235,11 @@ public class SampleQueueTest extends TestCase { sampleQueue.rewind(); assertAllocationCount(0); // Can't read again. - assertEquals(8, sampleQueue.getReadIndex()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(8); assertReadEndOfStream(false); } + @Test public void testAdvanceToEnd() { writeTestData(); sampleQueue.advanceToEnd(); @@ -233,6 +253,7 @@ public class SampleQueueTest extends TestCase { assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testAdvanceToEndRetainsUnassignedData() { sampleQueue.format(TEST_FORMAT_1); sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE); @@ -256,56 +277,62 @@ public class SampleQueueTest extends TestCase { assertAllocationCount(0); } + @Test public void testAdvanceToBeforeBuffer() { writeTestData(); int skipCount = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0] - 1, true, false); // Should fail and have no effect. - assertEquals(SampleQueue.ADVANCE_FAILED, skipCount); + assertThat(skipCount).isEqualTo(ADVANCE_FAILED); assertReadTestData(); assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testAdvanceToStartOfBuffer() { writeTestData(); int skipCount = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0], true, false); // Should succeed but have no effect (we're already at the first frame). - assertEquals(0, skipCount); + assertThat(skipCount).isEqualTo(0); assertReadTestData(); assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testAdvanceToEndOfBuffer() { writeTestData(); int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false); // Should succeed and skip to 2nd keyframe (the 4th frame). - assertEquals(4, skipCount); + assertThat(skipCount).isEqualTo(4); assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX); assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testAdvanceToAfterBuffer() { writeTestData(); int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, false); // Should fail and have no effect. - assertEquals(SampleQueue.ADVANCE_FAILED, skipCount); + assertThat(skipCount).isEqualTo(ADVANCE_FAILED); assertReadTestData(); assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testAdvanceToAfterBufferAllowed() { writeTestData(); int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true); // Should succeed and skip to 2nd keyframe (the 4th frame). - assertEquals(4, skipCount); + assertThat(skipCount).isEqualTo(4); assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX); assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testDiscardToEnd() { writeTestData(); // Should discard everything. sampleQueue.discardToEnd(); - assertEquals(8, sampleQueue.getReadIndex()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(8); assertAllocationCount(0); // We should still be able to read the upstream format. assertReadFormat(false, TEST_FORMAT_2); @@ -314,17 +341,18 @@ public class SampleQueueTest extends TestCase { assertReadTestData(TEST_FORMAT_2); } + @Test public void testDiscardToStopAtReadPosition() { writeTestData(); // Shouldn't discard anything. sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true); - assertEquals(0, sampleQueue.getReadIndex()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(0); 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()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(1); assertAllocationCount(10); // Should discard the read sample. sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1], false, true); @@ -334,7 +362,7 @@ public class SampleQueueTest extends TestCase { assertAllocationCount(9); // Should be able to read the remaining samples. assertReadTestData(TEST_FORMAT_1, 1, 7); - assertEquals(8, sampleQueue.getReadIndex()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(8); // Should discard up to the second last sample sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP - 1, false, true); assertAllocationCount(3); @@ -343,20 +371,22 @@ public class SampleQueueTest extends TestCase { assertAllocationCount(1); } + @Test public void testDiscardToDontStopAtReadPosition() { writeTestData(); // Shouldn't discard anything. sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1] - 1, false, false); - assertEquals(0, sampleQueue.getReadIndex()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(0); assertAllocationCount(10); // Should discard the first sample. sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1], false, false); - assertEquals(1, sampleQueue.getReadIndex()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(1); assertAllocationCount(9); // Should be able to read the remaining samples. assertReadTestData(TEST_FORMAT_1, 1, 7); } + @Test public void testDiscardUpstream() { writeTestData(); sampleQueue.discardUpstreamSamples(8); @@ -381,6 +411,7 @@ public class SampleQueueTest extends TestCase { assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testDiscardUpstreamMulti() { writeTestData(); sampleQueue.discardUpstreamSamples(4); @@ -391,6 +422,7 @@ public class SampleQueueTest extends TestCase { assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testDiscardUpstreamBeforeRead() { writeTestData(); sampleQueue.discardUpstreamSamples(4); @@ -400,6 +432,7 @@ public class SampleQueueTest extends TestCase { assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testDiscardUpstreamAfterRead() { writeTestData(); assertReadTestData(null, 0, 3); @@ -421,41 +454,44 @@ public class SampleQueueTest extends TestCase { assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testLargestQueuedTimestampWithDiscardUpstream() { writeTestData(); - assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP); 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()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()) + .isEqualTo(TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 2]); sampleQueue.discardUpstreamSamples(0); // Discarding everything from upstream without reading should unset the largest timestamp. - assertEquals(Long.MIN_VALUE, sampleQueue.getLargestQueuedTimestampUs()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(MIN_VALUE); } + @Test 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()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(7000); 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()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(7000); sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 3); // Once a third sample is discarded, the largest timestamp should have changed. - assertEquals(4000, sampleQueue.getLargestQueuedTimestampUs()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(4000); sampleQueue.discardUpstreamSamples(0); // Discarding everything from upstream without reading should unset the largest timestamp. - assertEquals(Long.MIN_VALUE, sampleQueue.getLargestQueuedTimestampUs()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(MIN_VALUE); } + @Test public void testLargestQueuedTimestampWithRead() { writeTestData(); - assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP); assertReadTestData(); // Reading everything should not reduce the largest timestamp. - assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP); } // Internal methods. @@ -580,9 +616,9 @@ public class SampleQueueTest extends TestCase { private void assertReadNothing(boolean formatRequired) { clearFormatHolderAndInputBuffer(); int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0); - assertEquals(C.RESULT_NOTHING_READ, result); + assertThat(result).isEqualTo(RESULT_NOTHING_READ); // formatHolder should not be populated. - assertNull(formatHolder.format); + assertThat(formatHolder.format).isNull(); // inputBuffer should not be populated. assertInputBufferContainsNoSampleData(); assertInputBufferHasNoDefaultFlagsSet(); @@ -597,14 +633,14 @@ public class SampleQueueTest extends TestCase { private void assertReadEndOfStream(boolean formatRequired) { clearFormatHolderAndInputBuffer(); int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, true, 0); - assertEquals(C.RESULT_BUFFER_READ, result); + assertThat(result).isEqualTo(RESULT_BUFFER_READ); // formatHolder should not be populated. - assertNull(formatHolder.format); + assertThat(formatHolder.format).isNull(); // inputBuffer should not contain sample data, but end of stream flag should be set. assertInputBufferContainsNoSampleData(); - assertTrue(inputBuffer.isEndOfStream()); - assertFalse(inputBuffer.isDecodeOnly()); - assertFalse(inputBuffer.isEncrypted()); + assertThat(inputBuffer.isEndOfStream()).isTrue(); + assertThat(inputBuffer.isDecodeOnly()).isFalse(); + assertThat(inputBuffer.isEncrypted()).isFalse(); } /** @@ -617,9 +653,9 @@ public class SampleQueueTest extends TestCase { private void assertReadFormat(boolean formatRequired, Format format) { clearFormatHolderAndInputBuffer(); int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0); - assertEquals(C.RESULT_FORMAT_READ, result); + assertThat(result).isEqualTo(RESULT_FORMAT_READ); // formatHolder should be populated. - assertEquals(format, formatHolder.format); + assertThat(formatHolder.format).isEqualTo(format); // inputBuffer should not be populated. assertInputBufferContainsNoSampleData(); assertInputBufferHasNoDefaultFlagsSet(); @@ -639,19 +675,19 @@ public class SampleQueueTest extends TestCase { int length) { clearFormatHolderAndInputBuffer(); int result = sampleQueue.read(formatHolder, inputBuffer, false, false, 0); - assertEquals(C.RESULT_BUFFER_READ, result); + assertThat(result).isEqualTo(RESULT_BUFFER_READ); // formatHolder should not be populated. - assertNull(formatHolder.format); + assertThat(formatHolder.format).isNull(); // inputBuffer should be populated. - assertEquals(timeUs, inputBuffer.timeUs); - assertEquals(isKeyframe, inputBuffer.isKeyFrame()); - assertFalse(inputBuffer.isDecodeOnly()); - assertFalse(inputBuffer.isEncrypted()); + assertThat(inputBuffer.timeUs).isEqualTo(timeUs); + assertThat(inputBuffer.isKeyFrame()).isEqualTo(isKeyframe); + assertThat(inputBuffer.isDecodeOnly()).isFalse(); + assertThat(inputBuffer.isEncrypted()).isFalse(); inputBuffer.flip(); - assertEquals(length, inputBuffer.data.limit()); + assertThat(inputBuffer.data.limit()).isEqualTo(length); byte[] readData = new byte[length]; inputBuffer.data.get(readData); - MoreAsserts.assertEquals(Arrays.copyOfRange(sampleData, offset, offset + length), readData); + assertThat(readData).isEqualTo(copyOfRange(sampleData, offset, offset + length)); } /** @@ -660,7 +696,7 @@ public class SampleQueueTest extends TestCase { * @param count The expected number of allocations. */ private void assertAllocationCount(int count) { - assertEquals(ALLOCATION_SIZE * count, allocator.getTotalBytesAllocated()); + assertThat(allocator.getTotalBytesAllocated()).isEqualTo(ALLOCATION_SIZE * count); } /** @@ -671,13 +707,13 @@ public class SampleQueueTest extends TestCase { return; } inputBuffer.flip(); - assertEquals(0, inputBuffer.data.limit()); + assertThat(inputBuffer.data.limit()).isEqualTo(0); } private void assertInputBufferHasNoDefaultFlagsSet() { - assertFalse(inputBuffer.isEndOfStream()); - assertFalse(inputBuffer.isDecodeOnly()); - assertFalse(inputBuffer.isEncrypted()); + assertThat(inputBuffer.isEndOfStream()).isFalse(); + assertThat(inputBuffer.isDecodeOnly()).isFalse(); + assertThat(inputBuffer.isEncrypted()).isFalse(); } private void clearFormatHolderAndInputBuffer() { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java similarity index 78% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java index 5de6bdf3e1..1229e47883 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java @@ -15,18 +15,27 @@ */ package com.google.android.exoplayer2.source; +import static com.google.android.exoplayer2.C.INDEX_UNSET; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link ShuffleOrder}. */ -public final class ShuffleOrderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ShuffleOrderTest { public static final long RANDOM_SEED = 1234567890L; + @Test public void testDefaultShuffleOrder() { assertShuffleOrderCorrectness(new DefaultShuffleOrder(0, RANDOM_SEED), 0); assertShuffleOrderCorrectness(new DefaultShuffleOrder(1, RANDOM_SEED), 1); @@ -44,6 +53,7 @@ public final class ShuffleOrderTest extends TestCase { testCloneAndRemove(new DefaultShuffleOrder(1, RANDOM_SEED), 0); } + @Test public void testUnshuffledShuffleOrder() { assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(0), 0); assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(1), 1); @@ -61,35 +71,36 @@ public final class ShuffleOrderTest extends TestCase { testCloneAndRemove(new UnshuffledShuffleOrder(1), 0); } + @Test public void testUnshuffledShuffleOrderIsUnshuffled() { ShuffleOrder shuffleOrder = new UnshuffledShuffleOrder(5); - assertEquals(0, shuffleOrder.getFirstIndex()); - assertEquals(4, shuffleOrder.getLastIndex()); + assertThat(shuffleOrder.getFirstIndex()).isEqualTo(0); + assertThat(shuffleOrder.getLastIndex()).isEqualTo(4); for (int i = 0; i < 4; i++) { - assertEquals(i + 1, shuffleOrder.getNextIndex(i)); + assertThat(shuffleOrder.getNextIndex(i)).isEqualTo(i + 1); } } private static void assertShuffleOrderCorrectness(ShuffleOrder shuffleOrder, int length) { - assertEquals(length, shuffleOrder.getLength()); + assertThat(shuffleOrder.getLength()).isEqualTo(length); if (length == 0) { - assertEquals(C.INDEX_UNSET, shuffleOrder.getFirstIndex()); - assertEquals(C.INDEX_UNSET, shuffleOrder.getLastIndex()); + assertThat(shuffleOrder.getFirstIndex()).isEqualTo(INDEX_UNSET); + assertThat(shuffleOrder.getLastIndex()).isEqualTo(INDEX_UNSET); } else { int[] indices = new int[length]; indices[0] = shuffleOrder.getFirstIndex(); - assertEquals(C.INDEX_UNSET, shuffleOrder.getPreviousIndex(indices[0])); + assertThat(shuffleOrder.getPreviousIndex(indices[0])).isEqualTo(INDEX_UNSET); for (int i = 1; i < length; i++) { indices[i] = shuffleOrder.getNextIndex(indices[i - 1]); - assertEquals(indices[i - 1], shuffleOrder.getPreviousIndex(indices[i])); + assertThat(shuffleOrder.getPreviousIndex(indices[i])).isEqualTo(indices[i - 1]); for (int j = 0; j < i; j++) { - assertTrue(indices[i] != indices[j]); + assertThat(indices[i] != indices[j]).isTrue(); } } - assertEquals(indices[length - 1], shuffleOrder.getLastIndex()); - assertEquals(C.INDEX_UNSET, shuffleOrder.getNextIndex(indices[length - 1])); + assertThat(shuffleOrder.getLastIndex()).isEqualTo(indices[length - 1]); + assertThat(shuffleOrder.getNextIndex(indices[length - 1])).isEqualTo(INDEX_UNSET); for (int i = 0; i < length; i++) { - assertTrue(indices[i] >= 0 && indices[i] < length); + assertThat(indices[i] >= 0 && indices[i] < length).isTrue(); } } } @@ -107,7 +118,7 @@ public final class ShuffleOrderTest extends TestCase { while (newNextIndex >= position && newNextIndex < position + count) { newNextIndex = newOrder.getNextIndex(newNextIndex); } - assertEquals(expectedNextIndex, newNextIndex); + assertThat(newNextIndex).isEqualTo(expectedNextIndex); } } @@ -127,7 +138,7 @@ public final class ShuffleOrderTest extends TestCase { expectedNextIndex--; } int newNextIndex = newOrder.getNextIndex(i < position ? i : i - 1); - assertEquals(expectedNextIndex, newNextIndex); + assertThat(newNextIndex).isEqualTo(expectedNextIndex); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java similarity index 59% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java index 82dc6b4ad5..557611c4ea 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java @@ -15,43 +15,62 @@ */ package com.google.android.exoplayer2.text.ttml; +import static android.graphics.Color.BLACK; +import static android.graphics.Color.RED; +import static android.graphics.Color.YELLOW; +import static com.google.android.exoplayer2.text.ttml.TtmlRenderUtil.resolveStyle; +import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD; +import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD_ITALIC; +import static com.google.common.truth.Truth.assertThat; + import android.graphics.Color; -import android.test.InstrumentationTestCase; import java.util.HashMap; import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** - * Unit test for TtmlRenderUtil + * Unit test for {@link TtmlRenderUtil}. */ -public class TtmlRenderUtilTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class TtmlRenderUtilTest { + @Test public void testResolveStyleNoStyleAtAll() { - assertNull(TtmlRenderUtil.resolveStyle(null, null, null)); + assertThat(resolveStyle(null, null, null)).isNull(); } + + @Test public void testResolveStyleSingleReferentialStyle() { Map globalStyles = getGlobalStyles(); String[] styleIds = {"s0"}; - assertSame(globalStyles.get("s0"), - TtmlRenderUtil.resolveStyle(null, styleIds, globalStyles)); + assertThat(TtmlRenderUtil.resolveStyle(null, styleIds, globalStyles)) + .isSameAs(globalStyles.get("s0")); } + + @Test public void testResolveStyleMultipleReferentialStyles() { Map globalStyles = getGlobalStyles(); String[] styleIds = {"s0", "s1"}; TtmlStyle resolved = TtmlRenderUtil.resolveStyle(null, styleIds, globalStyles); - assertNotSame(globalStyles.get("s0"), resolved); - assertNotSame(globalStyles.get("s1"), resolved); - assertNull(resolved.getId()); + assertThat(resolved).isNotSameAs(globalStyles.get("s0")); + assertThat(resolved).isNotSameAs(globalStyles.get("s1")); + assertThat(resolved.getId()).isNull(); // inherited from s0 - assertEquals(Color.BLACK, resolved.getBackgroundColor()); + assertThat(resolved.getBackgroundColor()).isEqualTo(BLACK); // inherited from s1 - assertEquals(Color.RED, resolved.getFontColor()); + assertThat(resolved.getFontColor()).isEqualTo(RED); // merged from s0 and s1 - assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, resolved.getStyle()); + assertThat(resolved.getStyle()).isEqualTo(STYLE_BOLD_ITALIC); } + @Test public void testResolveMergeSingleReferentialStyleIntoInlineStyle() { Map globalStyles = getGlobalStyles(); String[] styleIds = {"s0"}; @@ -59,15 +78,15 @@ public class TtmlRenderUtilTest extends InstrumentationTestCase { style.setBackgroundColor(Color.YELLOW); TtmlStyle resolved = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); - assertSame(style, resolved); + assertThat(resolved).isSameAs(style); // inline attribute not overridden - assertEquals(Color.YELLOW, resolved.getBackgroundColor()); + assertThat(resolved.getBackgroundColor()).isEqualTo(YELLOW); // inherited from referential style - assertEquals(TtmlStyle.STYLE_BOLD, resolved.getStyle()); + assertThat(resolved.getStyle()).isEqualTo(STYLE_BOLD); } - + @Test public void testResolveMergeMultipleReferentialStylesIntoInlineStyle() { Map globalStyles = getGlobalStyles(); String[] styleIds = {"s0", "s1"}; @@ -75,20 +94,21 @@ public class TtmlRenderUtilTest extends InstrumentationTestCase { style.setBackgroundColor(Color.YELLOW); TtmlStyle resolved = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); - assertSame(style, resolved); + assertThat(resolved).isSameAs(style); // inline attribute not overridden - assertEquals(Color.YELLOW, resolved.getBackgroundColor()); + assertThat(resolved.getBackgroundColor()).isEqualTo(YELLOW); // inherited from both referential style - assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, resolved.getStyle()); + assertThat(resolved.getStyle()).isEqualTo(STYLE_BOLD_ITALIC); } + @Test public void testResolveStyleOnlyInlineStyle() { TtmlStyle inlineStyle = new TtmlStyle(); - assertSame(inlineStyle, TtmlRenderUtil.resolveStyle(inlineStyle, null, null)); + assertThat(TtmlRenderUtil.resolveStyle(inlineStyle, null, null)).isSameAs(inlineStyle); } - private Map getGlobalStyles() { + private static Map getGlobalStyles() { Map globalStyles = new HashMap<>(); TtmlStyle s0 = new TtmlStyle(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java new file mode 100644 index 0000000000..4c35e259ff --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java @@ -0,0 +1,155 @@ +/* + * 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.text.ttml; + +import static android.graphics.Color.BLACK; +import static android.graphics.Color.WHITE; +import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD; +import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD_ITALIC; +import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_ITALIC; +import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_NORMAL; +import static com.google.android.exoplayer2.text.ttml.TtmlStyle.UNSPECIFIED; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import android.graphics.Color; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Unit test for {@link TtmlStyle}. */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class TtmlStyleTest { + + private static final String FONT_FAMILY = "serif"; + private static final String ID = "id"; + public static final int FOREGROUND_COLOR = Color.WHITE; + public static final int BACKGROUND_COLOR = Color.BLACK; + private TtmlStyle style; + + @Before + public void setUp() throws Exception { + style = new TtmlStyle(); + } + + @Test + public void testInheritStyle() { + style.inherit(createAncestorStyle()); + assertWithMessage("id must not be inherited").that(style.getId()).isNull(); + assertThat(style.isUnderline()).isTrue(); + assertThat(style.isLinethrough()).isTrue(); + assertThat(style.getStyle()).isEqualTo(STYLE_BOLD_ITALIC); + assertThat(style.getFontFamily()).isEqualTo(FONT_FAMILY); + assertThat(style.getFontColor()).isEqualTo(WHITE); + assertWithMessage("do not inherit backgroundColor").that(style.hasBackgroundColor()).isFalse(); + } + + @Test + public void testChainStyle() { + style.chain(createAncestorStyle()); + assertWithMessage("id must not be inherited").that(style.getId()).isNull(); + assertThat(style.isUnderline()).isTrue(); + assertThat(style.isLinethrough()).isTrue(); + assertThat(style.getStyle()).isEqualTo(STYLE_BOLD_ITALIC); + assertThat(style.getFontFamily()).isEqualTo(FONT_FAMILY); + assertThat(style.getFontColor()).isEqualTo(FOREGROUND_COLOR); + // do inherit backgroundColor when chaining + assertWithMessage("do not inherit backgroundColor when chaining") + .that(style.getBackgroundColor()).isEqualTo(BACKGROUND_COLOR); + } + + private static TtmlStyle createAncestorStyle() { + TtmlStyle ancestor = new TtmlStyle(); + ancestor.setId(ID); + ancestor.setItalic(true); + ancestor.setBold(true); + ancestor.setBackgroundColor(BACKGROUND_COLOR); + ancestor.setFontColor(FOREGROUND_COLOR); + ancestor.setLinethrough(true); + ancestor.setUnderline(true); + ancestor.setFontFamily(FONT_FAMILY); + return ancestor; + } + + @Test + public void testStyle() { + assertThat(style.getStyle()).isEqualTo(UNSPECIFIED); + style.setItalic(true); + assertThat(style.getStyle()).isEqualTo(STYLE_ITALIC); + style.setBold(true); + assertThat(style.getStyle()).isEqualTo(STYLE_BOLD_ITALIC); + style.setItalic(false); + assertThat(style.getStyle()).isEqualTo(STYLE_BOLD); + style.setBold(false); + assertThat(style.getStyle()).isEqualTo(STYLE_NORMAL); + } + + @Test + public void testLinethrough() { + assertThat(style.isLinethrough()).isFalse(); + style.setLinethrough(true); + assertThat(style.isLinethrough()).isTrue(); + style.setLinethrough(false); + assertThat(style.isLinethrough()).isFalse(); + } + + @Test + public void testUnderline() { + assertThat(style.isUnderline()).isFalse(); + style.setUnderline(true); + assertThat(style.isUnderline()).isTrue(); + style.setUnderline(false); + assertThat(style.isUnderline()).isFalse(); + } + + @Test + public void testFontFamily() { + assertThat(style.getFontFamily()).isNull(); + style.setFontFamily(FONT_FAMILY); + assertThat(style.getFontFamily()).isEqualTo(FONT_FAMILY); + style.setFontFamily(null); + assertThat(style.getFontFamily()).isNull(); + } + + @Test + public void testColor() { + assertThat(style.hasFontColor()).isFalse(); + style.setFontColor(Color.BLACK); + assertThat(style.getFontColor()).isEqualTo(BLACK); + assertThat(style.hasFontColor()).isTrue(); + } + + @Test + public void testBackgroundColor() { + assertThat(style.hasBackgroundColor()).isFalse(); + style.setBackgroundColor(Color.BLACK); + assertThat(style.getBackgroundColor()).isEqualTo(BLACK); + assertThat(style.hasBackgroundColor()).isTrue(); + } + + @Test + public void testId() { + assertThat(style.getId()).isNull(); + style.setId(ID); + assertThat(style.getId()).isEqualTo(ID); + style.setId(null); + assertThat(style.getId()).isNull(); + } + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java similarity index 61% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java index d6be100877..6ade85be28 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java @@ -15,22 +15,32 @@ */ package com.google.android.exoplayer2.text.webvtt; -import android.test.InstrumentationTestCase; +import static com.google.android.exoplayer2.text.webvtt.CssParser.parseNextToken; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link CssParser}. */ -public final class CssParserTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class CssParserTest { private CssParser parser; - @Override + @Before public void setUp() { parser = new CssParser(); } + @Test public void testSkipWhitespacesAndComments() { // Skip only whitespaces String skipOnlyWhitespaces = " \t\r\n\f End of skip\n /* */"; @@ -53,6 +63,7 @@ public final class CssParserTest extends InstrumentationTestCase { assertSkipsToEndOfSkip(null, skipEverything); } + @Test public void testGetInputLimit() { // \r After 3 lines. String threeLinesThen3Cr = "One Line\nThen other\rAnd finally\r\r\r"; @@ -78,6 +89,7 @@ public final class CssParserTest extends InstrumentationTestCase { assertInputLimit(null, ""); } + @Test public void testParseMethodSimpleInput() { String styleBlock1 = " ::cue { color : black; background-color: PapayaWhip }"; WebvttCssStyle expectedStyle = new WebvttCssStyle(); @@ -96,6 +108,7 @@ public final class CssParserTest extends InstrumentationTestCase { assertParserProduces(expectedStyle, styleBlock3); } + @Test public void testMultiplePropertiesInBlock() { String styleBlock = "::cue(#id){text-decoration:underline; background-color:green;" + "color:red; font-family:Courier; font-weight:bold}"; @@ -110,6 +123,7 @@ public final class CssParserTest extends InstrumentationTestCase { assertParserProduces(expectedStyle, styleBlock); } + @Test public void testRgbaColorExpression() { String styleBlock = "::cue(#rgb){background-color: rgba(\n10/* Ugly color */,11\t, 12\n,.1);" + "color:rgb(1,1,\n1)}"; @@ -121,59 +135,62 @@ public final class CssParserTest extends InstrumentationTestCase { assertParserProduces(expectedStyle, styleBlock); } + @Test public void testGetNextToken() { 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("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)); + assertThat(parseNextToken(input, builder)).isEqualTo("lorem"); + assertThat(parseNextToken(input, builder)).isEqualTo(":"); + assertThat(parseNextToken(input, builder)).isEqualTo("ipsum"); + assertThat(parseNextToken(input, builder)).isEqualTo("{"); + assertThat(parseNextToken(input, builder)).isEqualTo("dolor"); + assertThat(parseNextToken(input, builder)).isEqualTo("}"); + assertThat(parseNextToken(input, builder)).isEqualTo("#sit"); + assertThat(parseNextToken(input, builder)).isEqualTo(","); + assertThat(parseNextToken(input, builder)).isEqualTo("amet"); + assertThat(parseNextToken(input, builder)).isEqualTo(";"); + assertThat(parseNextToken(input, builder)).isEqualTo("lorem"); + assertThat(parseNextToken(input, builder)).isEqualTo(":"); + assertThat(parseNextToken(input, builder)).isEqualTo("ipsum"); + assertThat(parseNextToken(input, builder)).isEqualTo("dolor"); + assertThat(parseNextToken(input, builder)).isEqualTo("("); + assertThat(parseNextToken(input, builder)).isEqualTo("("); + assertThat(parseNextToken(input, builder)).isEqualTo(")"); + assertThat(parseNextToken(input, builder)).isEqualTo(")"); + assertThat(parseNextToken(input, builder)).isNull(); } + @Test public void testStyleScoreSystem() { WebvttCssStyle style = new WebvttCssStyle(); // Universal selector. - assertEquals(1, style.getSpecificityScore("", "", new String[0], "")); + assertThat(style.getSpecificityScore("", "", new String[0], "")).isEqualTo(1); // Class match without tag match. style.setTargetClasses(new String[] { "class1", "class2"}); - assertEquals(8, style.getSpecificityScore("", "", new String[] { "class1", "class2", "class3" }, - "")); + assertThat(style.getSpecificityScore("", "", new String[]{"class1", "class2", "class3"}, + "")).isEqualTo(8); // Class and tag match style.setTargetTagName("b"); - assertEquals(10, style.getSpecificityScore("", "b", - new String[] { "class1", "class2", "class3" }, "")); + assertThat(style.getSpecificityScore("", "b", + new String[]{"class1", "class2", "class3"}, "")).isEqualTo(10); // Class insufficiency. - assertEquals(0, style.getSpecificityScore("", "b", new String[] { "class1", "class" }, "")); + assertThat(style.getSpecificityScore("", "b", new String[]{"class1", "class"}, "")) + .isEqualTo(0); // Voice, classes and tag match. style.setTargetVoice("Manuel Cráneo"); - assertEquals(14, style.getSpecificityScore("", "b", - new String[] { "class1", "class2", "class3" }, "Manuel Cráneo")); + assertThat(style.getSpecificityScore("", "b", + new String[]{"class1", "class2", "class3"}, "Manuel Cráneo")).isEqualTo(14); // Voice mismatch. - assertEquals(0, style.getSpecificityScore(null, "b", - new String[] { "class1", "class2", "class3" }, "Manuel Craneo")); + assertThat(style.getSpecificityScore(null, "b", + new String[]{"class1", "class2", "class3"}, "Manuel Craneo")).isEqualTo(0); // Id, voice, classes and tag match. style.setTargetId("id"); - assertEquals(0x40000000 + 14, style.getSpecificityScore("id", "b", - new String[] { "class1", "class2", "class3" }, "Manuel Cráneo")); + assertThat(style.getSpecificityScore("id", "b", + new String[]{"class1", "class2", "class3"}, "Manuel Cráneo")).isEqualTo(0x40000000 + 14); // Id mismatch. - assertEquals(0, style.getSpecificityScore("id1", "b", - new String[] { "class1", "class2", "class3" }, "")); + assertThat(style.getSpecificityScore("id1", "b", + new String[]{"class1", "class2", "class3"}, "")).isEqualTo(0); } // Utility methods. @@ -181,34 +198,34 @@ public final class CssParserTest extends InstrumentationTestCase { private void assertSkipsToEndOfSkip(String expectedLine, String s) { ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(s)); CssParser.skipWhitespaceAndComments(input); - assertEquals(expectedLine, input.readLine()); + assertThat(input.readLine()).isEqualTo(expectedLine); } private void assertInputLimit(String expectedLine, String s) { ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(s)); CssParser.skipStyleBlock(input); - assertEquals(expectedLine, input.readLine()); + assertThat(input.readLine()).isEqualTo(expectedLine); } private void assertParserProduces(WebvttCssStyle expected, String styleBlock){ ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(styleBlock)); WebvttCssStyle actualElem = parser.parseBlock(input); - assertEquals(expected.hasBackgroundColor(), actualElem.hasBackgroundColor()); + assertThat(actualElem.hasBackgroundColor()).isEqualTo(expected.hasBackgroundColor()); if (expected.hasBackgroundColor()) { - assertEquals(expected.getBackgroundColor(), actualElem.getBackgroundColor()); + assertThat(actualElem.getBackgroundColor()).isEqualTo(expected.getBackgroundColor()); } - assertEquals(expected.hasFontColor(), actualElem.hasFontColor()); + assertThat(actualElem.hasFontColor()).isEqualTo(expected.hasFontColor()); if (expected.hasFontColor()) { - assertEquals(expected.getFontColor(), actualElem.getFontColor()); + assertThat(actualElem.getFontColor()).isEqualTo(expected.getFontColor()); } - assertEquals(expected.getFontFamily(), actualElem.getFontFamily()); - assertEquals(expected.getFontSize(), actualElem.getFontSize()); - assertEquals(expected.getFontSizeUnit(), actualElem.getFontSizeUnit()); - assertEquals(expected.getStyle(), actualElem.getStyle()); - assertEquals(expected.isLinethrough(), actualElem.isLinethrough()); - assertEquals(expected.isUnderline(), actualElem.isUnderline()); - assertEquals(expected.getTextAlign(), actualElem.getTextAlign()); + assertThat(actualElem.getFontFamily()).isEqualTo(expected.getFontFamily()); + assertThat(actualElem.getFontSize()).isEqualTo(expected.getFontSize()); + assertThat(actualElem.getFontSizeUnit()).isEqualTo(expected.getFontSizeUnit()); + assertThat(actualElem.getStyle()).isEqualTo(expected.getStyle()); + assertThat(actualElem.isLinethrough()).isEqualTo(expected.isLinethrough()); + assertThat(actualElem.isUnderline()).isEqualTo(expected.isUnderline()); + assertThat(actualElem.getTextAlign()).isEqualTo(expected.getTextAlign()); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java similarity index 82% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java index 2cdad081c5..8937007990 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java @@ -15,16 +15,24 @@ */ package com.google.android.exoplayer2.text.webvtt; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; import java.util.List; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link Mp4WebvttDecoder}. */ -public final class Mp4WebvttDecoderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class Mp4WebvttDecoderTest { private static final byte[] SINGLE_CUE_SAMPLE = { 0x00, 0x00, 0x00, 0x1C, // Size @@ -79,6 +87,7 @@ public final class Mp4WebvttDecoderTest extends TestCase { // Positive tests. + @Test public void testSingleCueSample() throws SubtitleDecoderException { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); Subtitle result = decoder.decode(SINGLE_CUE_SAMPLE, SINGLE_CUE_SAMPLE.length, false); @@ -86,6 +95,7 @@ public final class Mp4WebvttDecoderTest extends TestCase { assertMp4WebvttSubtitleEquals(result, expectedCue); } + @Test public void testTwoCuesSample() throws SubtitleDecoderException { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); Subtitle result = decoder.decode(DOUBLE_CUE_SAMPLE, DOUBLE_CUE_SAMPLE.length, false); @@ -94,6 +104,7 @@ public final class Mp4WebvttDecoderTest extends TestCase { assertMp4WebvttSubtitleEquals(result, firstExpectedCue, secondExpectedCue); } + @Test public void testNoCueSample() throws SubtitleDecoderException { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); Subtitle result = decoder.decode(NO_CUE_SAMPLE, NO_CUE_SAMPLE.length, false); @@ -102,6 +113,7 @@ public final class Mp4WebvttDecoderTest extends TestCase { // Negative tests. + @Test public void testSampleWithIncompleteHeader() { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); try { @@ -122,10 +134,10 @@ public final class Mp4WebvttDecoderTest extends TestCase { * @param expectedCues The expected {@link Cue}s. */ private static void assertMp4WebvttSubtitleEquals(Subtitle subtitle, Cue... expectedCues) { - assertEquals(1, subtitle.getEventTimeCount()); - assertEquals(0, subtitle.getEventTime(0)); + assertThat(subtitle.getEventTimeCount()).isEqualTo(1); + assertThat(subtitle.getEventTime(0)).isEqualTo(0); List subtitleCues = subtitle.getCues(0); - assertEquals(expectedCues.length, subtitleCues.size()); + assertThat(subtitleCues).hasSize(expectedCues.length); for (int i = 0; i < subtitleCues.size(); i++) { assertCueEquals(expectedCues[i], subtitleCues.get(i)); } @@ -135,14 +147,14 @@ public final class Mp4WebvttDecoderTest extends TestCase { * Asserts that two cues are equal. */ private static void assertCueEquals(Cue expected, Cue actual) { - assertEquals(expected.line, actual.line); - assertEquals(expected.lineAnchor, actual.lineAnchor); - assertEquals(expected.lineType, actual.lineType); - assertEquals(expected.position, actual.position); - assertEquals(expected.positionAnchor, actual.positionAnchor); - assertEquals(expected.size, actual.size); - assertEquals(expected.text.toString(), actual.text.toString()); - assertEquals(expected.textAlignment, actual.textAlignment); + assertThat(actual.line).isEqualTo(expected.line); + assertThat(actual.lineAnchor).isEqualTo(expected.lineAnchor); + assertThat(actual.lineType).isEqualTo(expected.lineType); + assertThat(actual.position).isEqualTo(expected.position); + assertThat(actual.positionAnchor).isEqualTo(expected.positionAnchor); + assertThat(actual.size).isEqualTo(expected.size); + assertThat(actual.text.toString()).isEqualTo(expected.text.toString()); + assertThat(actual.textAlignment).isEqualTo(expected.textAlignment); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java similarity index 50% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java index 1ee8976a7e..2a6e461627 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java @@ -15,209 +15,235 @@ */ package com.google.android.exoplayer2.text.webvtt; +import static android.graphics.Typeface.BOLD; +import static android.graphics.Typeface.ITALIC; +import static com.google.common.truth.Truth.assertThat; + import android.graphics.Typeface; -import android.test.InstrumentationTestCase; import android.text.Spanned; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import java.util.Collections; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link WebvttCueParser}. */ -public final class WebvttCueParserTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class WebvttCueParserTest { + @Test public void testParseStrictValidClassesAndTrailingTokens() throws Exception { Spanned text = parseCueText("" + "This is text with html tags"); - assertEquals("This is text with html tags", text.toString()); + assertThat(text.toString()).isEqualTo("This is text with html tags"); UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class); StyleSpan[] styleSpans = getSpans(text, StyleSpan.class); - assertEquals(1, underlineSpans.length); - assertEquals(2, styleSpans.length); - assertEquals(Typeface.ITALIC, styleSpans[0].getStyle()); - assertEquals(Typeface.BOLD, styleSpans[1].getStyle()); + assertThat(underlineSpans).hasLength(1); + assertThat(styleSpans).hasLength(2); + assertThat(styleSpans[0].getStyle()).isEqualTo(ITALIC); + assertThat(styleSpans[1].getStyle()).isEqualTo(BOLD); - assertEquals(5, text.getSpanStart(underlineSpans[0])); - assertEquals(7, text.getSpanEnd(underlineSpans[0])); - assertEquals(18, text.getSpanStart(styleSpans[0])); - assertEquals(18, text.getSpanStart(styleSpans[1])); - assertEquals(22, text.getSpanEnd(styleSpans[0])); - assertEquals(22, text.getSpanEnd(styleSpans[1])); + assertThat(text.getSpanStart(underlineSpans[0])).isEqualTo(5); + assertThat(text.getSpanEnd(underlineSpans[0])).isEqualTo(7); + assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(18); + assertThat(text.getSpanStart(styleSpans[1])).isEqualTo(18); + assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(22); + assertThat(text.getSpanEnd(styleSpans[1])).isEqualTo(22); } + @Test public void testParseStrictValidUnsupportedTagsStrippedOut() throws Exception { Spanned text = parseCueText("This is text with " + "html tags"); - assertEquals("This is text with html tags", text.toString()); - assertEquals(0, getSpans(text, UnderlineSpan.class).length); - assertEquals(0, getSpans(text, StyleSpan.class).length); + assertThat(text.toString()).isEqualTo("This is text with html tags"); + assertThat(getSpans(text, UnderlineSpan.class)).hasLength(0); + assertThat(getSpans(text, StyleSpan.class)).hasLength(0); } + @Test public void testParseWellFormedUnclosedEndAtCueEnd() throws Exception { Spanned text = parseCueText("An unclosed u tag with " + "italic inside"); - assertEquals("An unclosed u tag with italic inside", text.toString()); + assertThat(text.toString()).isEqualTo("An unclosed u tag with italic inside"); UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class); StyleSpan[] styleSpans = getSpans(text, StyleSpan.class); - assertEquals(1, underlineSpans.length); - assertEquals(1, styleSpans.length); - assertEquals(Typeface.ITALIC, styleSpans[0].getStyle()); + assertThat(underlineSpans).hasLength(1); + assertThat(styleSpans).hasLength(1); + assertThat(styleSpans[0].getStyle()).isEqualTo(ITALIC); - assertEquals(3, text.getSpanStart(underlineSpans[0])); - assertEquals(23, text.getSpanStart(styleSpans[0])); - assertEquals(29, text.getSpanEnd(styleSpans[0])); - assertEquals(36, text.getSpanEnd(underlineSpans[0])); + assertThat(text.getSpanStart(underlineSpans[0])).isEqualTo(3); + assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(23); + assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(29); + assertThat(text.getSpanEnd(underlineSpans[0])).isEqualTo(36); } + @Test public void testParseWellFormedUnclosedEndAtParent() throws Exception { Spanned text = parseCueText("An unclosed u tag with underline and italic inside"); - assertEquals("An unclosed u tag with underline and italic inside", text.toString()); + assertThat(text.toString()).isEqualTo("An unclosed u tag with underline and italic inside"); UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class); StyleSpan[] styleSpans = getSpans(text, StyleSpan.class); - assertEquals(1, underlineSpans.length); - assertEquals(1, styleSpans.length); + assertThat(underlineSpans).hasLength(1); + assertThat(styleSpans).hasLength(1); - assertEquals(23, text.getSpanStart(underlineSpans[0])); - assertEquals(23, text.getSpanStart(styleSpans[0])); - assertEquals(43, text.getSpanEnd(underlineSpans[0])); - assertEquals(43, text.getSpanEnd(styleSpans[0])); + assertThat(text.getSpanStart(underlineSpans[0])).isEqualTo(23); + assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(23); + assertThat(text.getSpanEnd(underlineSpans[0])).isEqualTo(43); + assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(43); - assertEquals(Typeface.ITALIC, styleSpans[0].getStyle()); + assertThat(styleSpans[0].getStyle()).isEqualTo(ITALIC); } + @Test public void testParseMalformedNestedElements() throws Exception { Spanned text = parseCueText("An unclosed u tag with italic inside"); - assertEquals("An unclosed u tag with italic inside", text.toString()); + assertThat(text.toString()).isEqualTo("An unclosed u tag with italic inside"); UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class); StyleSpan[] styleSpans = getSpans(text, StyleSpan.class); - assertEquals(1, underlineSpans.length); - assertEquals(2, styleSpans.length); + assertThat(underlineSpans).hasLength(1); + assertThat(styleSpans).hasLength(2); // all tags applied until matching start tag found - assertEquals(0, text.getSpanStart(underlineSpans[0])); - assertEquals(29, text.getSpanEnd(underlineSpans[0])); + assertThat(text.getSpanStart(underlineSpans[0])).isEqualTo(0); + assertThat(text.getSpanEnd(underlineSpans[0])).isEqualTo(29); if (styleSpans[0].getStyle() == Typeface.BOLD) { - assertEquals(0, text.getSpanStart(styleSpans[0])); - assertEquals(23, text.getSpanStart(styleSpans[1])); - assertEquals(29, text.getSpanEnd(styleSpans[1])); - assertEquals(36, text.getSpanEnd(styleSpans[0])); + assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(0); + assertThat(text.getSpanStart(styleSpans[1])).isEqualTo(23); + assertThat(text.getSpanEnd(styleSpans[1])).isEqualTo(29); + assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(36); } else { - assertEquals(0, text.getSpanStart(styleSpans[1])); - assertEquals(23, text.getSpanStart(styleSpans[0])); - assertEquals(29, text.getSpanEnd(styleSpans[0])); - assertEquals(36, text.getSpanEnd(styleSpans[1])); + assertThat(text.getSpanStart(styleSpans[1])).isEqualTo(0); + assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(23); + assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(29); + assertThat(text.getSpanEnd(styleSpans[1])).isEqualTo(36); } } + @Test public void testParseCloseNonExistingTag() throws Exception { Spanned text = parseCueText("blahblahblahblah"); - assertEquals("blahblahblahblah", text.toString()); + assertThat(text.toString()).isEqualTo("blahblahblahblah"); StyleSpan[] spans = getSpans(text, StyleSpan.class); - assertEquals(1, spans.length); - assertEquals(Typeface.BOLD, spans[0].getStyle()); - assertEquals(4, text.getSpanStart(spans[0])); - assertEquals(8, text.getSpanEnd(spans[0])); // should be 12 when valid + assertThat(spans).hasLength(1); + assertThat(spans[0].getStyle()).isEqualTo(BOLD); + assertThat(text.getSpanStart(spans[0])).isEqualTo(4); + assertThat(text.getSpanEnd(spans[0])).isEqualTo(8); // should be 12 when valid } + @Test public void testParseEmptyTagName() throws Exception { Spanned text = parseCueText("An unclosed u tag with <>italic inside"); - assertEquals("An unclosed u tag with italic inside", text.toString()); + assertThat(text.toString()).isEqualTo("An unclosed u tag with italic inside"); } + @Test public void testParseEntities() throws Exception { Spanned text = parseCueText("& > <  "); - assertEquals("& > < ", text.toString()); + assertThat(text.toString()).isEqualTo("& > < "); } + @Test public void testParseEntitiesUnsupported() throws Exception { Spanned text = parseCueText("&noway; &sure;"); - assertEquals(" ", text.toString()); + assertThat(text.toString()).isEqualTo(" "); } + @Test public void testParseEntitiesNotTerminated() throws Exception { Spanned text = parseCueText("& here comes text"); - assertEquals("& here comes text", text.toString()); + assertThat(text.toString()).isEqualTo("& here comes text"); } + @Test public void testParseEntitiesNotTerminatedUnsupported() throws Exception { Spanned text = parseCueText("&surenot here comes text"); - assertEquals(" here comes text", text.toString()); + assertThat(text.toString()).isEqualTo(" here comes text"); } + @Test public void testParseEntitiesNotTerminatedNoSpace() throws Exception { Spanned text = parseCueText("&surenot"); - assertEquals("&surenot", text.toString()); + assertThat(text.toString()).isEqualTo("&surenot"); } + @Test public void testParseVoidTag() throws Exception { Spanned text = parseCueText("here comes
    text
    "); - assertEquals("here comes text", text.toString()); + assertThat(text.toString()).isEqualTo("here comes text"); } + @Test public void testParseMultipleTagsOfSameKind() { Spanned text = parseCueText("blah blah blah foo"); - assertEquals("blah blah blah foo", text.toString()); + assertThat(text.toString()).isEqualTo("blah blah blah foo"); StyleSpan[] spans = getSpans(text, StyleSpan.class); - assertEquals(2, spans.length); - assertEquals(5, text.getSpanStart(spans[0])); - assertEquals(9, text.getSpanEnd(spans[0])); - assertEquals(15, text.getSpanStart(spans[1])); - assertEquals(18, text.getSpanEnd(spans[1])); - assertEquals(Typeface.BOLD, spans[0].getStyle()); - assertEquals(Typeface.BOLD, spans[1].getStyle()); + assertThat(spans).hasLength(2); + assertThat(text.getSpanStart(spans[0])).isEqualTo(5); + assertThat(text.getSpanEnd(spans[0])).isEqualTo(9); + assertThat(text.getSpanStart(spans[1])).isEqualTo(15); + assertThat(text.getSpanEnd(spans[1])).isEqualTo(18); + assertThat(spans[0].getStyle()).isEqualTo(BOLD); + assertThat(spans[1].getStyle()).isEqualTo(BOLD); } + @Test public void testParseInvalidVoidSlash() { Spanned text = parseCueText("blah blah"); - assertEquals("blah blah", text.toString()); + assertThat(text.toString()).isEqualTo("blah blah"); StyleSpan[] spans = getSpans(text, StyleSpan.class); - assertEquals(0, spans.length); + assertThat(spans).hasLength(0); } + @Test public void testParseMonkey() throws Exception { Spanned text = parseCueText("< u>An unclosed u tag with <<<<< i>italic
    " + " inside"); - assertEquals("An unclosed u tag with italic inside", text.toString()); + assertThat(text.toString()).isEqualTo("An unclosed u tag with italic inside"); text = parseCueText(">>>>>>>>>An unclosed u tag with <<<<< italic" + " inside"); - assertEquals(">>>>>>>>>An unclosed u tag with inside", text.toString()); + assertThat(text.toString()).isEqualTo(">>>>>>>>>An unclosed u tag with inside"); } + @Test public void testParseCornerCases() throws Exception { Spanned text = parseCueText(">"); - assertEquals(">", text.toString()); + assertThat(text.toString()).isEqualTo(">"); text = parseCueText("<"); - assertEquals("", text.toString()); + assertThat(text.toString()).isEmpty(); text = parseCueText("><<<<<<<<<<"); - assertEquals(">", text.toString()); + assertThat(text.toString()).isEqualTo(">"); text = parseCueText("<>"); - assertEquals("", text.toString()); + assertThat(text.toString()).isEmpty(); text = parseCueText("&"); - assertEquals("&", text.toString()); + assertThat(text.toString()).isEqualTo("&"); text = parseCueText("&&&&&&&"); - assertEquals("&&&&&&&", text.toString()); + assertThat(text.toString()).isEqualTo("&&&&&&&"); } private static Spanned parseCueText(String string) { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java similarity index 78% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java index 164c6c149a..c3c30e44a8 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java @@ -15,17 +15,25 @@ */ package com.google.android.exoplayer2.text.webvtt; -import com.google.android.exoplayer2.C; +import static com.google.android.exoplayer2.C.INDEX_UNSET; +import static com.google.common.truth.Truth.assertThat; +import static java.lang.Long.MAX_VALUE; + import com.google.android.exoplayer2.text.Cue; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link WebvttSubtitle}. */ -public class WebvttSubtitleTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class WebvttSubtitleTest { private static final String FIRST_SUBTITLE_STRING = "This is the first subtitle."; private static final String SECOND_SUBTITLE_STRING = "This is the second subtitle."; @@ -65,21 +73,25 @@ public class WebvttSubtitleTest extends TestCase { nestedSubtitle = new WebvttSubtitle(nestedSubtitleCues); } + @Test public void testEventCount() { - assertEquals(0, emptySubtitle.getEventTimeCount()); - assertEquals(4, simpleSubtitle.getEventTimeCount()); - assertEquals(4, overlappingSubtitle.getEventTimeCount()); - assertEquals(4, nestedSubtitle.getEventTimeCount()); + assertThat(emptySubtitle.getEventTimeCount()).isEqualTo(0); + assertThat(simpleSubtitle.getEventTimeCount()).isEqualTo(4); + assertThat(overlappingSubtitle.getEventTimeCount()).isEqualTo(4); + assertThat(nestedSubtitle.getEventTimeCount()).isEqualTo(4); } + @Test public void testSimpleSubtitleEventTimes() { testSubtitleEventTimesHelper(simpleSubtitle); } + @Test public void testSimpleSubtitleEventIndices() { testSubtitleEventIndicesHelper(simpleSubtitle); } + @Test public void testSimpleSubtitleText() { // Test before first subtitle assertSingleCueEmpty(simpleSubtitle.getCues(0)); @@ -107,14 +119,17 @@ public class WebvttSubtitleTest extends TestCase { assertSingleCueEmpty(simpleSubtitle.getCues(Long.MAX_VALUE)); } + @Test public void testOverlappingSubtitleEventTimes() { testSubtitleEventTimesHelper(overlappingSubtitle); } + @Test public void testOverlappingSubtitleEventIndices() { testSubtitleEventIndicesHelper(overlappingSubtitle); } + @Test public void testOverlappingSubtitleText() { // Test before first subtitle assertSingleCueEmpty(overlappingSubtitle.getCues(0)); @@ -145,14 +160,17 @@ public class WebvttSubtitleTest extends TestCase { assertSingleCueEmpty(overlappingSubtitle.getCues(Long.MAX_VALUE)); } + @Test public void testNestedSubtitleEventTimes() { testSubtitleEventTimesHelper(nestedSubtitle); } + @Test public void testNestedSubtitleEventIndices() { testSubtitleEventIndicesHelper(nestedSubtitle); } + @Test public void testNestedSubtitleText() { // Test before first subtitle assertSingleCueEmpty(nestedSubtitle.getCues(0)); @@ -181,46 +199,46 @@ public class WebvttSubtitleTest extends TestCase { } private void testSubtitleEventTimesHelper(WebvttSubtitle subtitle) { - assertEquals(1000000, subtitle.getEventTime(0)); - assertEquals(2000000, subtitle.getEventTime(1)); - assertEquals(3000000, subtitle.getEventTime(2)); - assertEquals(4000000, subtitle.getEventTime(3)); + assertThat(subtitle.getEventTime(0)).isEqualTo(1000000); + assertThat(subtitle.getEventTime(1)).isEqualTo(2000000); + assertThat(subtitle.getEventTime(2)).isEqualTo(3000000); + assertThat(subtitle.getEventTime(3)).isEqualTo(4000000); } private void testSubtitleEventIndicesHelper(WebvttSubtitle subtitle) { // Test first event - assertEquals(0, subtitle.getNextEventTimeIndex(0)); - assertEquals(0, subtitle.getNextEventTimeIndex(500000)); - assertEquals(0, subtitle.getNextEventTimeIndex(999999)); + assertThat(subtitle.getNextEventTimeIndex(0)).isEqualTo(0); + assertThat(subtitle.getNextEventTimeIndex(500000)).isEqualTo(0); + assertThat(subtitle.getNextEventTimeIndex(999999)).isEqualTo(0); // Test second event - assertEquals(1, subtitle.getNextEventTimeIndex(1000000)); - assertEquals(1, subtitle.getNextEventTimeIndex(1500000)); - assertEquals(1, subtitle.getNextEventTimeIndex(1999999)); + assertThat(subtitle.getNextEventTimeIndex(1000000)).isEqualTo(1); + assertThat(subtitle.getNextEventTimeIndex(1500000)).isEqualTo(1); + assertThat(subtitle.getNextEventTimeIndex(1999999)).isEqualTo(1); // Test third event - assertEquals(2, subtitle.getNextEventTimeIndex(2000000)); - assertEquals(2, subtitle.getNextEventTimeIndex(2500000)); - assertEquals(2, subtitle.getNextEventTimeIndex(2999999)); + assertThat(subtitle.getNextEventTimeIndex(2000000)).isEqualTo(2); + assertThat(subtitle.getNextEventTimeIndex(2500000)).isEqualTo(2); + assertThat(subtitle.getNextEventTimeIndex(2999999)).isEqualTo(2); // Test fourth event - assertEquals(3, subtitle.getNextEventTimeIndex(3000000)); - assertEquals(3, subtitle.getNextEventTimeIndex(3500000)); - assertEquals(3, subtitle.getNextEventTimeIndex(3999999)); + assertThat(subtitle.getNextEventTimeIndex(3000000)).isEqualTo(3); + assertThat(subtitle.getNextEventTimeIndex(3500000)).isEqualTo(3); + assertThat(subtitle.getNextEventTimeIndex(3999999)).isEqualTo(3); // Test null event (i.e. look for events after the last event) - assertEquals(C.INDEX_UNSET, subtitle.getNextEventTimeIndex(4000000)); - assertEquals(C.INDEX_UNSET, subtitle.getNextEventTimeIndex(4500000)); - assertEquals(C.INDEX_UNSET, subtitle.getNextEventTimeIndex(Long.MAX_VALUE)); + assertThat(subtitle.getNextEventTimeIndex(4000000)).isEqualTo(INDEX_UNSET); + assertThat(subtitle.getNextEventTimeIndex(4500000)).isEqualTo(INDEX_UNSET); + assertThat(subtitle.getNextEventTimeIndex(MAX_VALUE)).isEqualTo(INDEX_UNSET); } private void assertSingleCueEmpty(List cues) { - assertTrue(cues.size() == 0); + assertThat(cues).isEmpty(); } private void assertSingleCueTextEquals(String expected, List cues) { - assertTrue(cues.size() == 1); - assertEquals(expected, cues.get(0).text.toString()); + assertThat(cues).hasSize(1); + assertThat(cues.get(0).text.toString()).isEqualTo(expected); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java similarity index 87% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java index c31c651384..cffc530354 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.trackselection; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; @@ -22,12 +24,17 @@ 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; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit tests for {@link MappingTrackSelector}. */ -public final class MappingTrackSelectorTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class MappingTrackSelectorTest { private static final RendererCapabilities VIDEO_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_VIDEO); @@ -54,6 +61,7 @@ public final class MappingTrackSelectorTest extends TestCase { /** * Tests that the video and audio track groups are mapped onto the correct renderers. */ + @Test public void testMapping() throws ExoPlaybackException { FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(); trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); @@ -65,6 +73,7 @@ public final class MappingTrackSelectorTest extends TestCase { * Tests that the video and audio track groups are mapped onto the correct renderers when the * renderer ordering is reversed. */ + @Test public void testMappingReverseOrder() throws ExoPlaybackException { FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(); RendererCapabilities[] reverseOrderRendererCapabilities = new RendererCapabilities[] { @@ -78,6 +87,7 @@ public final class MappingTrackSelectorTest extends TestCase { * Tests video and audio track groups are mapped onto the correct renderers when there are * multiple track groups of the same type. */ + @Test public void testMappingMulti() throws ExoPlaybackException { FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(); TrackGroupArray multiTrackGroups = new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, @@ -92,46 +102,50 @@ public final class MappingTrackSelectorTest extends TestCase { * TrackGroupArray[], int[][][])} is propagated correctly to the result of * {@link MappingTrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray)}. */ + @Test 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)); + assertThat(result.selections.get(0)).isEqualTo(TRACK_SELECTIONS[0]); + assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); } /** * Tests that a null override clears a track selection. */ + @Test 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)); + assertThat(result.selections.get(0)).isNull(); + assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); } /** * Tests that a null override can be cleared. */ + @Test 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)); + assertThat(result.selections.get(0)).isEqualTo(TRACK_SELECTIONS[0]); + assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); } /** * Tests that an override is not applied for a different set of available track groups. */ + @Test 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)); + assertThat(result.selections.get(0)).isEqualTo(TRACK_SELECTIONS[0]); + assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); } /** @@ -156,9 +170,9 @@ public final class MappingTrackSelectorTest extends TestCase { } public void assertMappedTrackGroups(int rendererIndex, TrackGroup... expected) { - assertEquals(expected.length, lastRendererTrackGroupArrays[rendererIndex].length); + assertThat(lastRendererTrackGroupArrays[rendererIndex].length).isEqualTo(expected.length); for (int i = 0; i < expected.length; i++) { - assertEquals(expected[i], lastRendererTrackGroupArrays[rendererIndex].get(i)); + assertThat(lastRendererTrackGroupArrays[rendererIndex].get(i)).isEqualTo(expected[i]); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java similarity index 82% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java index b805ccbdd5..a72d060287 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java @@ -15,26 +15,37 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + import com.google.android.exoplayer2.C; import java.io.IOException; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit tests for {@link ByteArrayDataSource}. */ -public class ByteArrayDataSourceTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ByteArrayDataSourceTest { private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; private static final byte[] TEST_DATA_ODD = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + @Test public void testFullReadSingleBytes() { readTestData(TEST_DATA, 0, C.LENGTH_UNSET, 1, 0, 1, false); } + @Test public void testFullReadAllBytes() { readTestData(TEST_DATA, 0, C.LENGTH_UNSET, 100, 0, 100, false); } + @Test public void testLimitReadSingleBytes() { // Limit set to the length of the data. readTestData(TEST_DATA, 0, TEST_DATA.length, 1, 0, 1, false); @@ -42,6 +53,7 @@ public class ByteArrayDataSourceTest extends TestCase { readTestData(TEST_DATA, 0, 6, 1, 0, 1, false); } + @Test public void testFullReadTwoBytes() { // Try with the total data length an exact multiple of the size of each individual read. readTestData(TEST_DATA, 0, C.LENGTH_UNSET, 2, 0, 2, false); @@ -49,6 +61,7 @@ public class ByteArrayDataSourceTest extends TestCase { readTestData(TEST_DATA_ODD, 0, C.LENGTH_UNSET, 2, 0, 2, false); } + @Test public void testLimitReadTwoBytes() { // Try with the limit an exact multiple of the size of each individual read. readTestData(TEST_DATA, 0, 6, 2, 0, 2, false); @@ -56,6 +69,7 @@ public class ByteArrayDataSourceTest extends TestCase { readTestData(TEST_DATA, 0, 7, 2, 0, 2, false); } + @Test public void testReadFromValidOffsets() { // Read from an offset without bound. readTestData(TEST_DATA, 1, C.LENGTH_UNSET, 1, 0, 1, false); @@ -67,6 +81,7 @@ public class ByteArrayDataSourceTest extends TestCase { readTestData(TEST_DATA, TEST_DATA.length - 1, 1, 1, 0, 1, false); } + @Test public void testReadFromInvalidOffsets() { // Read from first invalid offset and check failure without bound. readTestData(TEST_DATA, TEST_DATA.length, C.LENGTH_UNSET, 1, 0, 1, true); @@ -74,6 +89,7 @@ public class ByteArrayDataSourceTest extends TestCase { readTestData(TEST_DATA, TEST_DATA.length, 1, 1, 0, 1, true); } + @Test public void testReadWithInvalidLength() { // Read more data than is available. readTestData(TEST_DATA, 0, TEST_DATA.length + 1, 1, 0, 1, true); @@ -102,10 +118,10 @@ public class ByteArrayDataSourceTest extends TestCase { // Open the source. long length = dataSource.open(new DataSpec(null, dataOffset, dataLength, null)); opened = true; - assertFalse(expectFailOnOpen); + assertThat(expectFailOnOpen).isFalse(); // Verify the resolved length is as we expect. - assertEquals(expectedFinalBytesRead, length); + assertThat(length).isEqualTo(expectedFinalBytesRead); byte[] outputBuffer = new byte[outputBufferLength]; int accumulatedBytesRead = 0; @@ -113,26 +129,26 @@ public class ByteArrayDataSourceTest extends TestCase { // Calculate a valid length for the next read, constraining by the specified output buffer // length, write offset and maximum write length input parameters. int requestedReadLength = Math.min(maxReadLength, outputBufferLength - writeOffset); - assertTrue(requestedReadLength > 0); + assertThat(requestedReadLength).isGreaterThan(0); int bytesRead = dataSource.read(outputBuffer, writeOffset, requestedReadLength); if (bytesRead != C.RESULT_END_OF_INPUT) { - assertTrue(bytesRead > 0); - assertTrue(bytesRead <= requestedReadLength); + assertThat(bytesRead).isGreaterThan(0); + assertThat(bytesRead).isAtMost(requestedReadLength); // Check the data read was correct. for (int i = 0; i < bytesRead; i++) { - assertEquals(testData[dataOffset + accumulatedBytesRead + i], - outputBuffer[writeOffset + i]); + assertThat(outputBuffer[writeOffset + i]) + .isEqualTo(testData[dataOffset + accumulatedBytesRead + i]); } // Check that we haven't read more data than we were expecting. accumulatedBytesRead += bytesRead; - assertTrue(accumulatedBytesRead <= expectedFinalBytesRead); + assertThat(accumulatedBytesRead).isAtMost(expectedFinalBytesRead); // If we haven't read all of the bytes the request should have been satisfied in full. - assertTrue(accumulatedBytesRead == expectedFinalBytesRead - || bytesRead == requestedReadLength); + assertThat(accumulatedBytesRead == expectedFinalBytesRead + || bytesRead == requestedReadLength).isTrue(); } else { // We're done. Check we read the expected number of bytes. - assertEquals(expectedFinalBytesRead, accumulatedBytesRead); + assertThat(accumulatedBytesRead).isEqualTo(expectedFinalBytesRead); return; } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java similarity index 61% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java index 5ba9e18e7d..85c4341232 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java @@ -15,50 +15,65 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.C.RESULT_END_OF_INPUT; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + import android.net.Uri; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; -import junit.framework.TestCase; +import java.nio.charset.Charset; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit tests for {@link DataSchemeDataSource}. */ -public final class DataSchemeDataSourceTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class DataSchemeDataSourceTest { private DataSource schemeDataDataSource; - @Override + @Before public void setUp() { schemeDataDataSource = new DataSchemeDataSource(); } + @Test public void testBase64Data() throws IOException { DataSpec dataSpec = buildDataSpec("data:text/plain;base64,eyJwcm92aWRlciI6IndpZGV2aW5lX3Rlc3QiL" + "CJjb250ZW50X2lkIjoiTWpBeE5WOTBaV0Z5Y3c9PSIsImtleV9pZHMiOlsiMDAwMDAwMDAwMDAwMDAwMDAwMDAwM" + "DAwMDAwMDAwMDAiXX0="); - TestUtil.assertDataSourceContent(schemeDataDataSource, dataSpec, + DataSourceAsserts.assertDataSourceContent(schemeDataDataSource, dataSpec, ("{\"provider\":\"widevine_test\",\"content_id\":\"MjAxNV90ZWFycw==\",\"key_ids\":" - + "[\"00000000000000000000000000000000\"]}").getBytes()); + + "[\"00000000000000000000000000000000\"]}").getBytes(Charset.forName(C.UTF8_NAME))); } + @Test public void testAsciiData() throws IOException { - TestUtil.assertDataSourceContent(schemeDataDataSource, buildDataSpec("data:,A%20brief%20note"), - "A brief note".getBytes()); + DataSourceAsserts.assertDataSourceContent(schemeDataDataSource, + buildDataSpec("data:,A%20brief%20note"), + "A brief note".getBytes(Charset.forName(C.UTF8_NAME))); } + @Test public void testPartialReads() throws IOException { byte[] buffer = new byte[18]; DataSpec dataSpec = buildDataSpec("data:,012345678901234567"); - assertEquals(18, schemeDataDataSource.open(dataSpec)); - assertEquals(9, schemeDataDataSource.read(buffer, 0, 9)); - assertEquals(0, schemeDataDataSource.read(buffer, 3, 0)); - assertEquals(9, schemeDataDataSource.read(buffer, 9, 15)); - assertEquals(0, schemeDataDataSource.read(buffer, 1, 0)); - assertEquals(C.RESULT_END_OF_INPUT, schemeDataDataSource.read(buffer, 1, 1)); - assertEquals("012345678901234567", new String(buffer, 0, 18)); + assertThat(schemeDataDataSource.open(dataSpec)).isEqualTo(18); + assertThat(schemeDataDataSource.read(buffer, 0, 9)).isEqualTo(9); + assertThat(schemeDataDataSource.read(buffer, 3, 0)).isEqualTo(0); + assertThat(schemeDataDataSource.read(buffer, 9, 15)).isEqualTo(9); + assertThat(schemeDataDataSource.read(buffer, 1, 0)).isEqualTo(0); + assertThat(schemeDataDataSource.read(buffer, 1, 1)).isEqualTo(RESULT_END_OF_INPUT); + assertThat(new String(buffer, 0, 18, C.UTF8_NAME)).isEqualTo("012345678901234567"); } + @Test public void testIncorrectScheme() { try { schemeDataDataSource.open(buildDataSpec("http://www.google.com")); @@ -68,6 +83,7 @@ public final class DataSchemeDataSourceTest extends TestCase { } } + @Test public void testMalformedData() { try { schemeDataDataSource.open(buildDataSpec("data:text/plain;base64,,This%20is%20Content")); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceAsserts.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceAsserts.java new file mode 100644 index 0000000000..eff3245923 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceAsserts.java @@ -0,0 +1,50 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.IOException; + +/** + * Assertions for data source tests. + */ +/* package */ final class DataSourceAsserts { + + /** + * 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); + assertThat(length).isEqualTo(expectedData.length); + byte[] readData = TestUtil.readToEnd(dataSource); + assertThat(readData).isEqualTo(expectedData); + } finally { + dataSource.close(); + } + } + + private DataSourceAsserts() {} + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java similarity index 65% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java index 38797ede66..8cd6c23fb1 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java @@ -15,75 +15,84 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.common.truth.Truth.assertThat; + import android.net.Uri; -import android.test.MoreAsserts; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; import java.util.Arrays; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit tests for {@link DataSourceInputStream}. */ -public class DataSourceInputStreamTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class DataSourceInputStreamTest { private static final byte[] TEST_DATA = TestUtil.buildTestData(16); + @Test public void testReadSingleBytes() throws IOException { DataSourceInputStream inputStream = buildTestInputStream(); // No bytes read yet. - assertEquals(0, inputStream.bytesRead()); + assertThat(inputStream.bytesRead()).isEqualTo(0); // Read bytes. for (int i = 0; i < TEST_DATA.length; i++) { int readByte = inputStream.read(); - assertTrue(0 <= readByte && readByte < 256); - assertEquals(TEST_DATA[i] & 0xFF, readByte); - assertEquals(i + 1, inputStream.bytesRead()); + assertThat(0 <= readByte && readByte < 256).isTrue(); + assertThat(readByte).isEqualTo(TEST_DATA[i] & 0xFF); + assertThat(inputStream.bytesRead()).isEqualTo(i + 1); } // Check end of stream. - assertEquals(-1, inputStream.read()); - assertEquals(TEST_DATA.length, inputStream.bytesRead()); + assertThat(inputStream.read()).isEqualTo(-1); + assertThat(inputStream.bytesRead()).isEqualTo(TEST_DATA.length); // Check close succeeds. inputStream.close(); } + @Test public void testRead() throws IOException { DataSourceInputStream inputStream = buildTestInputStream(); // Read bytes. byte[] readBytes = new byte[TEST_DATA.length]; int totalBytesRead = 0; while (totalBytesRead < TEST_DATA.length) { - long bytesRead = inputStream.read(readBytes, totalBytesRead, + int bytesRead = inputStream.read(readBytes, totalBytesRead, TEST_DATA.length - totalBytesRead); - assertTrue(bytesRead > 0); + assertThat(bytesRead).isGreaterThan(0); totalBytesRead += bytesRead; - assertEquals(totalBytesRead, inputStream.bytesRead()); + assertThat(inputStream.bytesRead()).isEqualTo(totalBytesRead); } // Check the read data. - MoreAsserts.assertEquals(TEST_DATA, readBytes); + assertThat(readBytes).isEqualTo(TEST_DATA); // Check end of stream. - assertEquals(TEST_DATA.length, inputStream.bytesRead()); - assertEquals(TEST_DATA.length, totalBytesRead); - assertEquals(-1, inputStream.read()); + assertThat(inputStream.bytesRead()).isEqualTo(TEST_DATA.length); + assertThat(totalBytesRead).isEqualTo(TEST_DATA.length); + assertThat(inputStream.read()).isEqualTo(-1); // Check close succeeds. inputStream.close(); } + @Test public void testSkip() throws IOException { DataSourceInputStream inputStream = buildTestInputStream(); // Skip bytes. long totalBytesSkipped = 0; while (totalBytesSkipped < TEST_DATA.length) { long bytesSkipped = inputStream.skip(Long.MAX_VALUE); - assertTrue(bytesSkipped > 0); + assertThat(bytesSkipped > 0).isTrue(); totalBytesSkipped += bytesSkipped; - assertEquals(totalBytesSkipped, inputStream.bytesRead()); + assertThat(inputStream.bytesRead()).isEqualTo(totalBytesSkipped); } // Check end of stream. - assertEquals(TEST_DATA.length, inputStream.bytesRead()); - assertEquals(TEST_DATA.length, totalBytesSkipped); - assertEquals(-1, inputStream.read()); + assertThat(inputStream.bytesRead()).isEqualTo(TEST_DATA.length); + assertThat(totalBytesSkipped).isEqualTo(TEST_DATA.length); + assertThat(inputStream.read()).isEqualTo(-1); // Check close succeeds. inputStream.close(); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java new file mode 100644 index 0000000000..aa98ad3179 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java @@ -0,0 +1,115 @@ +/* + * 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.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import android.net.Uri; +import com.google.android.exoplayer2.testutil.FakeDataSet; +import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData; +import com.google.android.exoplayer2.upstream.DataSourceInputStream; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; + +/** Assertion methods for {@link com.google.android.exoplayer2.upstream.cache.Cache}. */ +/* package */ 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(); + Uri[] uris = new Uri[allData.size()]; + for (int i = 0; i < allData.size(); i++) { + uris[i] = allData.get(i).uri; + } + assertCachedData(cache, fakeDataSet, uris); + } + + /** + * 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 { + Uri[] uris = new Uri[uriStrings.length]; + for (int i = 0; i < uriStrings.length; i++) { + uris[i] = Uri.parse(uriStrings[i]); + } + assertCachedData(cache, fakeDataSet, uris); + } + + /** + * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. + */ + public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, Uri... uris) + throws IOException { + int totalLength = 0; + for (Uri uri : uris) { + byte[] data = fakeDataSet.getData(uri).getData(); + assertDataCached(cache, uri, data); + totalLength += data.length; + } + assertThat(cache.getCacheSpace()).isEqualTo(totalLength); + } + + /** Asserts that the cache contains the given subset of data in the {@code fakeDataSet}. */ + public static void assertDataCached(Cache cache, FakeDataSet fakeDataSet, Uri... uris) + throws IOException { + for (Uri uri : uris) { + assertDataCached(cache, uri, fakeDataSet.getData(uri).getData()); + } + } + + /** Asserts that the cache contains the given data for {@code uriString}. */ + public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throws IOException { + CacheDataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, + new DataSpec(uri, 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(); + } + assertWithMessage("Cached data doesn't match expected for '" + uri + "'") + .that(outputStream.toByteArray()).isEqualTo(expected); + } + + /** Asserts that there is no cache content for the given {@code uriStrings}. */ + public static void assertDataNotCached(Cache cache, String... uriStrings) { + for (String uriString : uriStrings) { + assertWithMessage("There is cached data for '" + uriString + "'") + .that(cache.getCachedSpans(CacheUtil.generateKey(Uri.parse(uriString)))).isNull(); + } + } + + /** Asserts that the cache is empty. */ + public static void assertCacheEmpty(Cache cache) { + assertThat(cache.getCacheSpace()).isEqualTo(0); + } + + private CacheAsserts() {} + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java similarity index 77% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index e7ff2a6811..e92f072dc2 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -15,11 +15,16 @@ */ package com.google.android.exoplayer2.upstream.cache; -import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty; +import static android.net.Uri.EMPTY; +import static com.google.android.exoplayer2.C.LENGTH_UNSET; +import static com.google.android.exoplayer2.upstream.cache.CacheAsserts.assertCacheEmpty; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.util.Arrays.copyOf; +import static java.util.Arrays.copyOfRange; +import static org.junit.Assert.fail; import android.net.Uri; -import android.test.InstrumentationTestCase; -import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData; import com.google.android.exoplayer2.testutil.FakeDataSource; @@ -28,12 +33,21 @@ import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; -import java.util.Arrays; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; /** * Unit tests for {@link CacheDataSource}. */ -public class CacheDataSourceTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class CacheDataSourceTest { private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; private static final int MAX_CACHE_FILE_SIZE = 3; @@ -43,47 +57,52 @@ public class CacheDataSourceTest extends InstrumentationTestCase { private File tempFolder; private SimpleCache cache; - @Override + @Before public void setUp() throws Exception { - super.setUp(); - tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + tempFolder = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); } - @Override + @After public void tearDown() throws Exception { Util.recursiveDelete(tempFolder); - super.tearDown(); } + @Test public void testMaxCacheFileSize() throws Exception { CacheDataSource cacheDataSource = createCacheDataSource(false, false); assertReadDataContentLength(cacheDataSource, false, false); 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); + assertThat(cacheSpan.length <= MAX_CACHE_FILE_SIZE).isTrue(); + assertThat(cacheSpan.file.length() <= MAX_CACHE_FILE_SIZE).isTrue(); } } } + @Test public void testCacheAndRead() throws Exception { assertCacheAndRead(false, false); } + @Test public void testCacheAndReadUnboundedRequest() throws Exception { assertCacheAndRead(true, false); } + @Test public void testCacheAndReadUnknownLength() throws Exception { assertCacheAndRead(false, true); } // Disabled test as we don't support caching of definitely unknown length content + @Ignore + @Test public void disabledTestCacheAndReadUnboundedRequestUnknownLength() throws Exception { assertCacheAndRead(true, true); } + @Test public void testUnsatisfiableRange() throws Exception { // Bounded request but the content length is unknown. This forces all data to be cached but not // the length @@ -104,11 +123,12 @@ public class CacheDataSourceTest extends InstrumentationTestCase { } } + @Test public void testContentLengthEdgeCases() throws Exception { // 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, cache.getContentLength(KEY_1)); + assertThat(cache.getContentLength(KEY_1)).isEqualTo(LENGTH_UNSET); // 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. @@ -116,21 +136,23 @@ public class CacheDataSourceTest extends InstrumentationTestCase { assertReadDataContentLength(cacheDataSource, true, true); // Now the length set correctly do an unbounded request with offset - assertEquals(2, cacheDataSource.open(new DataSpec(Uri.EMPTY, TEST_DATA.length - 2, - C.LENGTH_UNSET, KEY_1))); + assertThat(cacheDataSource.open(new DataSpec(EMPTY, TEST_DATA.length - 2, + LENGTH_UNSET, KEY_1))).isEqualTo(2); // An unbounded request with offset for not cached content - assertEquals(C.LENGTH_UNSET, cacheDataSource.open(new DataSpec(Uri.EMPTY, TEST_DATA.length - 2, - C.LENGTH_UNSET, KEY_2))); + assertThat(cacheDataSource.open(new DataSpec(EMPTY, TEST_DATA.length - 2, + LENGTH_UNSET, KEY_2))).isEqualTo(LENGTH_UNSET); } + @Test public void testIgnoreCacheForUnsetLengthRequests() throws Exception { CacheDataSource cacheDataSource = createCacheDataSource(false, true, CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS); assertReadData(cacheDataSource, true, 0, C.LENGTH_UNSET); - MoreAsserts.assertEmpty(cache.getKeys()); + assertThat(cache.getKeys()).isEmpty(); } + @Test public void testReadOnlyCache() throws Exception { CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null); assertReadDataContentLength(cacheDataSource, false, false); @@ -157,9 +179,9 @@ public class CacheDataSourceTest extends InstrumentationTestCase { boolean unboundedRequest, boolean unknownLength) throws IOException { int length = unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length; 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, - cache.getContentLength(KEY_1)); + assertWithMessage("When the range specified, CacheDataSource doesn't reach EOS so shouldn't " + + "cache content length").that(cache.getContentLength(KEY_1)) + .isEqualTo(!unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length); } private void assertReadData(CacheDataSource cacheDataSource, boolean unknownLength, int position, @@ -168,8 +190,8 @@ public class CacheDataSourceTest extends InstrumentationTestCase { if (length != C.LENGTH_UNSET) { testDataLength = Math.min(testDataLength, length); } - assertEquals(unknownLength ? length : testDataLength, - cacheDataSource.open(new DataSpec(Uri.EMPTY, position, length, KEY_1))); + assertThat(cacheDataSource.open(new DataSpec(EMPTY, position, length, KEY_1))) + .isEqualTo(unknownLength ? length : testDataLength); byte[] buffer = new byte[100]; int totalBytesRead = 0; @@ -180,9 +202,9 @@ public class CacheDataSourceTest extends InstrumentationTestCase { } totalBytesRead += read; } - assertEquals(testDataLength, totalBytesRead); - MoreAsserts.assertEquals(Arrays.copyOfRange(TEST_DATA, position, position + testDataLength), - Arrays.copyOf(buffer, totalBytesRead)); + assertThat(totalBytesRead).isEqualTo(testDataLength); + assertThat(copyOf(buffer, totalBytesRead)) + .isEqualTo(copyOfRange(TEST_DATA, position, position + testDataLength)); cacheDataSource.close(); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java similarity index 86% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java index 7e8088f3be..3b8276c731 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java @@ -15,10 +15,12 @@ */ package com.google.android.exoplayer2.upstream.cache; +import static com.google.common.truth.Truth.assertThat; +import static java.util.Arrays.copyOf; +import static java.util.Arrays.copyOfRange; + import android.content.Context; import android.net.Uri; -import android.test.AndroidTestCase; -import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.TestUtil; @@ -32,13 +34,19 @@ import com.google.android.exoplayer2.upstream.crypto.AesCipherDataSource; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; -import java.util.Arrays; import java.util.Random; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; /** * Additional tests for {@link CacheDataSource}. */ -public class CacheDataSourceTest2 extends AndroidTestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class CacheDataSourceTest2 { private static final String EXO_CACHE_DIR = "exo"; private static final int EXO_CACHE_MAX_FILESIZE = 128; @@ -64,17 +72,20 @@ public class CacheDataSourceTest2 extends AndroidTestCase { private static final DataSpec START_OFF_BOUNDARY = new DataSpec(URI, OFFSET_OFF_BOUNDARY, DATA.length - OFFSET_OFF_BOUNDARY, KEY); + @Test public void testWithoutEncryption() throws IOException { testReads(false); } + @Test public void testWithEncryption() throws IOException { testReads(true); } private void testReads(boolean useEncryption) throws IOException { FakeDataSource upstreamSource = buildFakeUpstreamSource(); - CacheDataSource source = buildCacheDataSource(getContext(), upstreamSource, useEncryption); + CacheDataSource source = + buildCacheDataSource(RuntimeEnvironment.application, upstreamSource, useEncryption); // First read, should arrive from upstream. testRead(END_ON_BOUNDARY, source); assertSingleOpen(upstreamSource, 0, OFFSET_ON_BOUNDARY); @@ -110,8 +121,8 @@ public class CacheDataSourceTest2 extends AndroidTestCase { int maxBytesToRead = random.nextInt(scratch.length) + 1; bytesRead = source.read(scratch, 0, maxBytesToRead); if (bytesRead != C.RESULT_END_OF_INPUT) { - MoreAsserts.assertEquals(Arrays.copyOfRange(DATA, position, position + bytesRead), - Arrays.copyOf(scratch, bytesRead)); + assertThat(copyOf(scratch, bytesRead)) + .isEqualTo(copyOfRange(DATA, position, position + bytesRead)); position += bytesRead; } } @@ -124,10 +135,10 @@ public class CacheDataSourceTest2 extends AndroidTestCase { */ private void assertSingleOpen(FakeDataSource upstreamSource, int start, int end) { DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs(); - assertEquals(1, openedDataSpecs.length); - assertEquals(start, openedDataSpecs[0].position); - assertEquals(start, openedDataSpecs[0].absoluteStreamPosition); - assertEquals(end - start, openedDataSpecs[0].length); + assertThat(openedDataSpecs).hasLength(1); + assertThat(openedDataSpecs[0].position).isEqualTo(start); + assertThat(openedDataSpecs[0].absoluteStreamPosition).isEqualTo(start); + assertThat(openedDataSpecs[0].length).isEqualTo(end - start); } /** @@ -135,7 +146,7 @@ public class CacheDataSourceTest2 extends AndroidTestCase { */ private void assertNoOpen(FakeDataSource upstreamSource) { DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs(); - assertEquals(0, openedDataSpecs.length); + assertThat(openedDataSpecs).hasLength(0); } private static FakeDataSource buildFakeUpstreamSource() { @@ -177,7 +188,7 @@ public class CacheDataSourceTest2 extends AndroidTestCase { } } // Sanity check that the cache really is empty now. - assertTrue(cache.getKeys().isEmpty()); + assertThat(cache.getKeys().isEmpty()).isTrue(); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java similarity index 83% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java index df9975d43b..c8231ec4ac 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java @@ -15,11 +15,17 @@ */ 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 static android.net.Uri.EMPTY; +import static android.net.Uri.parse; +import static com.google.android.exoplayer2.C.LENGTH_UNSET; +import static com.google.android.exoplayer2.upstream.cache.CacheAsserts.assertCacheEmpty; +import static com.google.android.exoplayer2.upstream.cache.CacheAsserts.assertCachedData; +import static com.google.android.exoplayer2.upstream.cache.CacheUtil.generateKey; +import static com.google.android.exoplayer2.upstream.cache.CacheUtil.getKey; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import android.net.Uri; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; @@ -29,13 +35,23 @@ 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.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; /** * Tests {@link CacheUtil}. */ -public class CacheUtilTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class CacheUtilTest { /** * Abstract fake Cache implementation used by the test. This class must be public so Mockito can @@ -78,45 +94,46 @@ public class CacheUtilTest extends InstrumentationTestCase { private File tempFolder; private SimpleCache cache; - @Override + @Before public void setUp() throws Exception { - super.setUp(); - TestUtil.setUpMockito(this); + MockitoAnnotations.initMocks(this); mockCache.init(); - tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + tempFolder = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); } - @Override + @After public void tearDown() throws Exception { Util.recursiveDelete(tempFolder); - super.tearDown(); } + @Test public void testGenerateKey() throws Exception { - assertNotNull(CacheUtil.generateKey(Uri.EMPTY)); + assertThat(generateKey(EMPTY)).isNotNull(); Uri testUri = Uri.parse("test"); String key = CacheUtil.generateKey(testUri); - assertNotNull(key); + assertThat(key).isNotNull(); // Should generate the same key for the same input - assertEquals(key, CacheUtil.generateKey(testUri)); + assertThat(generateKey(testUri)).isEqualTo(key); // Should generate different key for different input - assertFalse(key.equals(CacheUtil.generateKey(Uri.parse("test2")))); + assertThat(key.equals(generateKey(parse("test2")))).isFalse(); } + @Test 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))); + assertThat(getKey(new DataSpec(testUri, 0, LENGTH_UNSET, key))).isEqualTo(key); // If not generates a new one using DataSpec.uri - assertEquals(CacheUtil.generateKey(testUri), - CacheUtil.getKey(new DataSpec(testUri, 0, C.LENGTH_UNSET, null))); + assertThat(getKey(new DataSpec(testUri, 0, LENGTH_UNSET, null))) + .isEqualTo(generateKey(testUri)); } + @Test public void testGetCachedNoData() throws Exception { CachingCounters counters = new CachingCounters(); CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, counters); @@ -124,6 +141,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCounters(counters, 0, 0, C.LENGTH_UNSET); } + @Test public void testGetCachedDataUnknownLength() throws Exception { // Mock there is 100 bytes cached at the beginning mockCache.spansAndGaps = new int[] {100}; @@ -133,6 +151,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCounters(counters, 100, 0, C.LENGTH_UNSET); } + @Test public void testGetCachedNoDataKnownLength() throws Exception { mockCache.contentLength = 1000; CachingCounters counters = new CachingCounters(); @@ -141,6 +160,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCounters(counters, 0, 0, 1000); } + @Test public void testGetCached() throws Exception { mockCache.contentLength = 1000; mockCache.spansAndGaps = new int[] {100, 100, 200}; @@ -150,6 +170,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCounters(counters, 300, 0, 1000); } + @Test public void testCache() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); @@ -161,6 +182,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testCacheSetOffsetAndLength() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); @@ -178,6 +200,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testCacheUnknownLength() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data") .setSimulateUnknownLength(true) @@ -192,6 +215,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testCacheUnknownLengthPartialCaching() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data") .setSimulateUnknownLength(true) @@ -211,6 +235,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testCacheLengthExceedsActualDataLength() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); @@ -224,6 +249,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testCacheThrowEOFException() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); @@ -241,6 +267,7 @@ public class CacheUtilTest extends InstrumentationTestCase { } } + @Test public void testCachePolling() throws Exception { final CachingCounters counters = new CachingCounters(); FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data") @@ -267,6 +294,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testRemove() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); @@ -283,9 +311,9 @@ public class CacheUtilTest extends InstrumentationTestCase { private static void assertCounters(CachingCounters counters, int alreadyCachedBytes, int newlyCachedBytes, int contentLength) { - assertEquals(alreadyCachedBytes, counters.alreadyCachedBytes); - assertEquals(newlyCachedBytes, counters.newlyCachedBytes); - assertEquals(contentLength, counters.contentLength); + assertThat(counters.alreadyCachedBytes).isEqualTo(alreadyCachedBytes); + assertThat(counters.newlyCachedBytes).isEqualTo(newlyCachedBytes); + assertThat(counters.contentLength).isEqualTo(contentLength); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java similarity index 69% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index 1a6beeb6ba..ed55045835 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2.upstream.cache; -import android.test.InstrumentationTestCase; -import android.test.MoreAsserts; +import static com.google.android.exoplayer2.C.LENGTH_UNSET; +import static com.google.android.exoplayer2.util.Util.toByteArray; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; import java.io.File; @@ -26,60 +28,71 @@ import java.io.IOException; import java.util.NavigableSet; import java.util.Random; import java.util.Set; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; /** * Unit tests for {@link SimpleCache}. */ -public class SimpleCacheTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class SimpleCacheTest { private static final String KEY_1 = "key1"; private File cacheDir; - @Override - protected void setUp() throws Exception { - cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + @Before + public void setUp() throws Exception { + cacheDir = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { Util.recursiveDelete(cacheDir); } + @Test public void testCommittingOneFile() throws Exception { SimpleCache simpleCache = getSimpleCache(); CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0); - assertFalse(cacheSpan1.isCached); - assertTrue(cacheSpan1.isOpenEnded()); + assertThat(cacheSpan1.isCached).isFalse(); + assertThat(cacheSpan1.isOpenEnded()).isTrue(); - assertNull(simpleCache.startReadWriteNonBlocking(KEY_1, 0)); + assertThat(simpleCache.startReadWriteNonBlocking(KEY_1, 0)).isNull(); - assertEquals(0, simpleCache.getKeys().size()); + assertThat(simpleCache.getKeys()).isEmpty(); NavigableSet cachedSpans = simpleCache.getCachedSpans(KEY_1); - assertTrue(cachedSpans == null || cachedSpans.size() == 0); - assertEquals(0, simpleCache.getCacheSpace()); - assertEquals(0, cacheDir.listFiles().length); + assertThat(cachedSpans == null || cachedSpans.isEmpty()).isTrue(); + assertThat(simpleCache.getCacheSpace()).isEqualTo(0); + assertThat(cacheDir.listFiles()).hasLength(0); addCache(simpleCache, KEY_1, 0, 15); Set cachedKeys = simpleCache.getKeys(); - assertEquals(1, cachedKeys.size()); - assertTrue(cachedKeys.contains(KEY_1)); + assertThat(cachedKeys).hasSize(1); + assertThat(cachedKeys.contains(KEY_1)).isTrue(); cachedSpans = simpleCache.getCachedSpans(KEY_1); - assertEquals(1, cachedSpans.size()); - assertTrue(cachedSpans.contains(cacheSpan1)); - assertEquals(15, simpleCache.getCacheSpace()); + assertThat(cachedSpans).hasSize(1); + assertThat(cachedSpans.contains(cacheSpan1)).isTrue(); + assertThat(simpleCache.getCacheSpace()).isEqualTo(15); simpleCache.releaseHoleSpan(cacheSpan1); CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0); - assertTrue(cacheSpan2.isCached); - assertFalse(cacheSpan2.isOpenEnded()); - assertEquals(15, cacheSpan2.length); + assertThat(cacheSpan2.isCached).isTrue(); + assertThat(cacheSpan2.isOpenEnded()).isFalse(); + assertThat(cacheSpan2.length).isEqualTo(15); assertCachedDataReadCorrect(cacheSpan2); } + @Test public void testReadCacheWithoutReleasingWriteCacheSpan() throws Exception { SimpleCache simpleCache = getSimpleCache(); @@ -90,19 +103,20 @@ public class SimpleCacheTest extends InstrumentationTestCase { simpleCache.releaseHoleSpan(cacheSpan1); } + @Test public void testSetGetLength() throws Exception { SimpleCache simpleCache = getSimpleCache(); - assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1)); + assertThat(simpleCache.getContentLength(KEY_1)).isEqualTo(LENGTH_UNSET); simpleCache.setContentLength(KEY_1, 15); - assertEquals(15, simpleCache.getContentLength(KEY_1)); + assertThat(simpleCache.getContentLength(KEY_1)).isEqualTo(15); simpleCache.startReadWrite(KEY_1, 0); addCache(simpleCache, KEY_1, 0, 15); simpleCache.setContentLength(KEY_1, 150); - assertEquals(150, simpleCache.getContentLength(KEY_1)); + assertThat(simpleCache.getContentLength(KEY_1)).isEqualTo(150); addCache(simpleCache, KEY_1, 140, 10); @@ -110,19 +124,20 @@ public class SimpleCacheTest extends InstrumentationTestCase { SimpleCache simpleCache2 = getSimpleCache(); Set keys = simpleCache.getKeys(); Set keys2 = simpleCache2.getKeys(); - assertEquals(keys, keys2); + assertThat(keys2).isEqualTo(keys); for (String key : keys) { - assertEquals(simpleCache.getContentLength(key), simpleCache2.getContentLength(key)); - assertEquals(simpleCache.getCachedSpans(key), simpleCache2.getCachedSpans(key)); + assertThat(simpleCache2.getContentLength(key)).isEqualTo(simpleCache.getContentLength(key)); + assertThat(simpleCache2.getCachedSpans(key)).isEqualTo(simpleCache.getCachedSpans(key)); } // Removing the last span shouldn't cause the length be change next time cache loaded SimpleCacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145); simpleCache2.removeSpan(lastSpan); simpleCache2 = getSimpleCache(); - assertEquals(150, simpleCache2.getContentLength(KEY_1)); + assertThat(simpleCache2.getContentLength(KEY_1)).isEqualTo(150); } + @Test public void testReloadCache() throws Exception { SimpleCache simpleCache = getSimpleCache(); @@ -139,6 +154,7 @@ public class SimpleCacheTest extends InstrumentationTestCase { assertCachedDataReadCorrect(cacheSpan2); } + @Test public void testEncryptedIndex() throws Exception { byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key SimpleCache simpleCache = getEncryptedSimpleCache(key); @@ -156,6 +172,7 @@ public class SimpleCacheTest extends InstrumentationTestCase { assertCachedDataReadCorrect(cacheSpan2); } + @Test public void testEncryptedIndexWrongKey() throws Exception { byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key SimpleCache simpleCache = getEncryptedSimpleCache(key); @@ -170,10 +187,11 @@ public class SimpleCacheTest extends InstrumentationTestCase { simpleCache = getEncryptedSimpleCache(key2); // Cache should be cleared - assertEquals(0, simpleCache.getKeys().size()); - assertEquals(0, cacheDir.listFiles().length); + assertThat(simpleCache.getKeys()).isEmpty(); + assertThat(cacheDir.listFiles()).hasLength(0); } + @Test public void testEncryptedIndexLostKey() throws Exception { byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key SimpleCache simpleCache = getEncryptedSimpleCache(key); @@ -187,41 +205,42 @@ public class SimpleCacheTest extends InstrumentationTestCase { simpleCache = getSimpleCache(); // Cache should be cleared - assertEquals(0, simpleCache.getKeys().size()); - assertEquals(0, cacheDir.listFiles().length); + assertThat(simpleCache.getKeys()).isEmpty(); + assertThat(cacheDir.listFiles()).hasLength(0); } + @Test public void testGetCachedBytes() throws Exception { SimpleCache simpleCache = getSimpleCache(); CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0); // No cached bytes, returns -'length' - assertEquals(-100, simpleCache.getCachedBytes(KEY_1, 0, 100)); + assertThat(simpleCache.getCachedBytes(KEY_1, 0, 100)).isEqualTo(-100); // Position value doesn't affect the return value - assertEquals(-100, simpleCache.getCachedBytes(KEY_1, 20, 100)); + assertThat(simpleCache.getCachedBytes(KEY_1, 20, 100)).isEqualTo(-100); addCache(simpleCache, KEY_1, 0, 15); // Returns the length of a single span - assertEquals(15, simpleCache.getCachedBytes(KEY_1, 0, 100)); + assertThat(simpleCache.getCachedBytes(KEY_1, 0, 100)).isEqualTo(15); // Value is capped by the 'length' - assertEquals(10, simpleCache.getCachedBytes(KEY_1, 0, 10)); + assertThat(simpleCache.getCachedBytes(KEY_1, 0, 10)).isEqualTo(10); addCache(simpleCache, KEY_1, 15, 35); // Returns the length of two adjacent spans - assertEquals(50, simpleCache.getCachedBytes(KEY_1, 0, 100)); + assertThat(simpleCache.getCachedBytes(KEY_1, 0, 100)).isEqualTo(50); addCache(simpleCache, KEY_1, 60, 10); // Not adjacent span doesn't affect return value - assertEquals(50, simpleCache.getCachedBytes(KEY_1, 0, 100)); + assertThat(simpleCache.getCachedBytes(KEY_1, 0, 100)).isEqualTo(50); // Returns length of hole up to the next cached span - assertEquals(-5, simpleCache.getCachedBytes(KEY_1, 55, 100)); + assertThat(simpleCache.getCachedBytes(KEY_1, 55, 100)).isEqualTo(-5); simpleCache.releaseHoleSpan(cacheSpan); } @@ -247,11 +266,11 @@ public class SimpleCacheTest extends InstrumentationTestCase { } private static void assertCachedDataReadCorrect(CacheSpan cacheSpan) throws IOException { - assertTrue(cacheSpan.isCached); + assertThat(cacheSpan.isCached).isTrue(); byte[] expected = generateData(cacheSpan.key, (int) cacheSpan.position, (int) cacheSpan.length); FileInputStream inputStream = new FileInputStream(cacheSpan.file); try { - MoreAsserts.assertEquals(expected, Util.toByteArray(inputStream)); + assertThat(toByteArray(inputStream)).isEqualTo(expected); } finally { inputStream.close(); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java similarity index 77% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java index b4e7e6e7f6..833a7e10c1 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java @@ -15,16 +15,25 @@ */ package com.google.android.exoplayer2.upstream.crypto; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.Util; import java.util.Random; import javax.crypto.Cipher; -import junit.framework.TestCase; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit tests for {@link AesFlushingCipher}. */ -public class AesFlushingCipherTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class AesFlushingCipherTest { private static final int DATA_LENGTH = 65536; private static final byte[] KEY = Util.getUtf8Bytes("testKey:12345678"); @@ -35,26 +44,26 @@ public class AesFlushingCipherTest extends TestCase { private AesFlushingCipher encryptCipher; private AesFlushingCipher decryptCipher; - @Override - protected void setUp() { + @Before + public void setUp() { encryptCipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, KEY, NONCE, START_OFFSET); decryptCipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, KEY, NONCE, START_OFFSET); } - @Override - protected void tearDown() { + @After + public void tearDown() { encryptCipher = null; decryptCipher = null; } - private long getMaxUnchangedBytesAllowedPostEncryption(long length) { + private static long getMaxUnchangedBytesAllowedPostEncryption(long length) { // Assuming that not more than 10% of the resultant bytes should be identical. // The value of 10% is arbitrary, ciphers standards do not name a value. return length / 10; } // Count the number of bytes that do not match. - private int getDifferingByteCount(byte[] data1, byte[] data2, int startOffset) { + private static int getDifferingByteCount(byte[] data1, byte[] data2, int startOffset) { int count = 0; for (int i = startOffset; i < data1.length; i++) { if (data1[i] != data2[i]) { @@ -65,25 +74,28 @@ public class AesFlushingCipherTest extends TestCase { } // Count the number of bytes that do not match. - private int getDifferingByteCount(byte[] data1, byte[] data2) { + private static int getDifferingByteCount(byte[] data1, byte[] data2) { return getDifferingByteCount(data1, data2, 0); } - // Test a single encrypt and decrypt call + // Test a single encrypt and decrypt call. + @Test public void testSingle() { byte[] reference = TestUtil.buildTestData(DATA_LENGTH); byte[] data = reference.clone(); encryptCipher.updateInPlace(data, 0, data.length); int unchangedByteCount = data.length - getDifferingByteCount(reference, data); - assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)); + assertThat(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)) + .isTrue(); decryptCipher.updateInPlace(data, 0, data.length); int differingByteCount = getDifferingByteCount(reference, data); - assertEquals(0, differingByteCount); + assertThat(differingByteCount).isEqualTo(0); } - // Test several encrypt and decrypt calls, each aligned on a 16 byte block size + // Test several encrypt and decrypt calls, each aligned on a 16 byte block size. + @Test public void testAligned() { byte[] reference = TestUtil.buildTestData(DATA_LENGTH); byte[] data = reference.clone(); @@ -93,28 +105,30 @@ public class AesFlushingCipherTest extends TestCase { while (offset < data.length) { int bytes = (1 + random.nextInt(50)) * 16; bytes = Math.min(bytes, data.length - offset); - assertEquals(0, bytes % 16); + assertThat(bytes % 16).isEqualTo(0); encryptCipher.updateInPlace(data, offset, bytes); offset += bytes; } int unchangedByteCount = data.length - getDifferingByteCount(reference, data); - assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)); + assertThat(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)) + .isTrue(); offset = 0; while (offset < data.length) { int bytes = (1 + random.nextInt(50)) * 16; bytes = Math.min(bytes, data.length - offset); - assertEquals(0, bytes % 16); + assertThat(bytes % 16).isEqualTo(0); decryptCipher.updateInPlace(data, offset, bytes); offset += bytes; } int differingByteCount = getDifferingByteCount(reference, data); - assertEquals(0, differingByteCount); + assertThat(differingByteCount).isEqualTo(0); } - // Test several encrypt and decrypt calls, not aligned on block boundary + // Test several encrypt and decrypt calls, not aligned on block boundary. + @Test public void testUnAligned() { byte[] reference = TestUtil.buildTestData(DATA_LENGTH); byte[] data = reference.clone(); @@ -130,7 +144,8 @@ public class AesFlushingCipherTest extends TestCase { } int unchangedByteCount = data.length - getDifferingByteCount(reference, data); - assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)); + assertThat(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)) + .isTrue(); offset = 0; while (offset < data.length) { @@ -141,10 +156,11 @@ public class AesFlushingCipherTest extends TestCase { } int differingByteCount = getDifferingByteCount(reference, data); - assertEquals(0, differingByteCount); + assertThat(differingByteCount).isEqualTo(0); } - // Test decryption starting from the middle of an encrypted block + // Test decryption starting from the middle of an encrypted block. + @Test public void testMidJoin() { byte[] reference = TestUtil.buildTestData(DATA_LENGTH); byte[] data = reference.clone(); @@ -161,7 +177,8 @@ public class AesFlushingCipherTest extends TestCase { // Verify int unchangedByteCount = data.length - getDifferingByteCount(reference, data); - assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)); + assertThat(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)) + .isTrue(); // Setup decryption from random location offset = random.nextInt(4096); @@ -180,7 +197,7 @@ public class AesFlushingCipherTest extends TestCase { // Verify int differingByteCount = getDifferingByteCount(reference, data, originalOffset); - assertEquals(0, differingByteCount); + assertThat(differingByteCount).isEqualTo(0); } } diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java new file mode 100644 index 0000000000..e7cd9baf59 --- /dev/null +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java @@ -0,0 +1,38 @@ +/* + * 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.dash; + +import android.test.InstrumentationTestCase; +import org.mockito.MockitoAnnotations; + +/** + * Utility for setting up Mockito for instrumentation tests. + */ +public final class MockitoUtil { + + /** + * Sets up Mockito for an instrumentation test. + */ + public static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { + // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. + System.setProperty("dexmaker.dexcache", + instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath()); + MockitoAnnotations.initMocks(instrumentationTestCase); + } + + private MockitoUtil() {} + +} diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java index f5e00d2cec..8532e65a68 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.DownloadException; import com.google.android.exoplayer2.offline.Downloader.ProgressListener; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.source.dash.MockitoUtil; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.testutil.FakeDataSet; @@ -54,7 +55,7 @@ public class DashDownloaderTest extends InstrumentationTestCase { @Override public void setUp() throws Exception { super.setUp(); - TestUtil.setUpMockito(this); + MockitoUtil.setUpMockito(this); tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/TestData.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java similarity index 99% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/TestData.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java index cef033bf17..88b5de7f65 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/TestData.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java @@ -13,19 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.extractor.ogg; +package com.google.android.exoplayer2.testutil; -import com.google.android.exoplayer2.testutil.FakeExtractorInput; -import com.google.android.exoplayer2.testutil.TestUtil; /** * Provides ogg/vorbis test data in bytes for unit tests. */ -/* package */ final class TestData { +public final class OggTestData { - /* package */ static FakeExtractorInput createInput(byte[] data, boolean simulateUnkownLength) { + public static FakeExtractorInput createInput(byte[] data, boolean simulateUnknownLength) { return new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true) - .setSimulateUnknownLength(simulateUnkownLength).setSimulatePartialReads(true).build(); + .setSimulateUnknownLength(simulateUnknownLength).setSimulatePartialReads(true).build(); } public static byte[] buildOggHeader(int headerType, long granule, int pageSequenceCounter, 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 2e59b33c0b..0f737ec23a 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 @@ -16,7 +16,6 @@ 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.Timeline; @@ -33,7 +32,6 @@ import java.io.InputStream; import java.util.Arrays; import java.util.Random; import junit.framework.Assert; -import org.mockito.MockitoAnnotations; /** * Utility methods for tests. @@ -121,13 +119,6 @@ public class TestUtil { return joined; } - public static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { - // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. - System.setProperty("dexmaker.dexcache", - instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath()); - MockitoAnnotations.initMocks(instrumentationTestCase); - } - public static byte[] getByteArray(Instrumentation instrumentation, String fileName) throws IOException { return Util.toByteArray(getInputStream(instrumentation, fileName)); From b2627d63fdb0215f65096ef2fe0c8e47c341e4a2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 8 Sep 2017 04:05:38 -0700 Subject: [PATCH 0071/1327] Allow ExoPlayerTestRunner to end when Player.stop() is called. In this case the playback state transitions to IDLE, which isn't caught so far. (This code is equivalent to the one in ExoHostedTest.java) ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167980981 --- .../android/exoplayer2/testutil/ExoPlayerTestRunner.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 2b5ea11d94..f65cb39bfc 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -212,6 +212,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener { private Exception exception; private TrackGroupArray trackGroups; private int positionDiscontinuityCount; + private boolean playerWasPrepared; private ExoPlayerTestRunner(PlayerFactory playerFactory, MediaSource mediaSource, RenderersFactory renderersFactory, MappingTrackSelector trackSelector, @@ -350,7 +351,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener { if (periodIndices.isEmpty() && playbackState == Player.STATE_READY) { periodIndices.add(player.getCurrentPeriodIndex()); } - if (playbackState == Player.STATE_ENDED) { + playerWasPrepared |= playbackState != Player.STATE_IDLE; + if (playbackState == Player.STATE_ENDED + || (playbackState == Player.STATE_IDLE && playerWasPrepared)) { endedCountDownLatch.countDown(); } } From 8f43dcd424c02ae15ceae578764d620409506a06 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 8 Sep 2017 07:34:07 -0700 Subject: [PATCH 0072/1327] Clear buffer in fake renderer in each iteration. This simulates reading from the buffer (which is what actual renderers would do). Otherwise the buffer always gets expanded and might cause memory issues. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167994899 --- .../com/google/android/exoplayer2/testutil/FakeRenderer.java | 1 + 1 file changed, 1 insertion(+) 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 index a66043b77f..c4270eb9c4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java @@ -59,6 +59,7 @@ public class FakeRenderer extends BaseRenderer { @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (!isEnded) { + buffer.clear(); // Verify the format matches the expected format. FormatHolder formatHolder = new FormatHolder(); int result = readSource(formatHolder, buffer, false); From ec38d0d8ab8cc0d810d16d052fbf700b8fc603d8 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 8 Sep 2017 07:54:10 -0700 Subject: [PATCH 0073/1327] Check thread is still alive before sending message in Loader. The release callback handler in Loader might not be alive anymore. Catch this case to prevent warnings about sending messages on dead threads. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167996538 --- .../java/com/google/android/exoplayer2/upstream/Loader.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 02ccfafa89..02e9a32116 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 @@ -420,7 +420,9 @@ public final class Loader implements LoaderErrorThrower { @Override public void run() { - sendEmptyMessage(0); + if (getLooper().getThread().isAlive()) { + sendEmptyMessage(0); + } } @Override From 40d443dc027c7a3e6ba45dd62ef6777906197dc1 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 8 Sep 2017 09:42:13 -0700 Subject: [PATCH 0074/1327] Enable rtmp in external demo app with extensions ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168007345 --- demos/main/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/demos/main/build.gradle b/demos/main/build.gradle index 099741d167..adad8f0e58 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -62,4 +62,5 @@ dependencies { withExtensionsCompile project(path: modulePrefix + 'extension-ima') withExtensionsCompile project(path: modulePrefix + 'extension-opus') withExtensionsCompile project(path: modulePrefix + 'extension-vp9') + withExtensionsCompile project(path: modulePrefix + 'extension-rtmp') } From 5a4155f09fee738da4ebd024a4f1b0a071d0fd42 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 8 Sep 2017 11:14:53 -0700 Subject: [PATCH 0075/1327] Destroy EGLSurface during DummySurface cleanup ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168020525 --- .../android/exoplayer2/video/DummySurface.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 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 20fe862dd2..450b4af38c 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 @@ -35,6 +35,7 @@ import static android.opengl.EGL14.eglChooseConfig; import static android.opengl.EGL14.eglCreateContext; import static android.opengl.EGL14.eglCreatePbufferSurface; import static android.opengl.EGL14.eglDestroyContext; +import static android.opengl.EGL14.eglDestroySurface; import static android.opengl.EGL14.eglGetDisplay; import static android.opengl.EGL14.eglInitialize; import static android.opengl.EGL14.eglMakeCurrent; @@ -175,8 +176,9 @@ public final class DummySurface extends Surface { private static final int MSG_RELEASE = 3; private final int[] textureIdHolder; - private EGLContext context; private EGLDisplay display; + private EGLContext context; + private EGLSurface pbuffer; private Handler handler; private SurfaceTexture surfaceTexture; @@ -315,7 +317,7 @@ public final class DummySurface extends Surface { EGL_HEIGHT, 1, EGL_NONE}; } - EGLSurface pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); + pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); boolean eglMadeCurrent = eglMakeCurrent(display, pbuffer, pbuffer, context); @@ -334,11 +336,15 @@ public final class DummySurface extends Surface { glDeleteTextures(1, textureIdHolder, 0); } } finally { + if (pbuffer != null) { + eglDestroySurface(display, pbuffer); + } if (context != null) { eglDestroyContext(display, context); } - display = null; + pbuffer = null; context = null; + display = null; surface = null; surfaceTexture = null; } From 74e8acf14f23fc60abe7c0a06dd3c03eeb3e2316 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 8 Aug 2017 10:54:06 -0700 Subject: [PATCH 0076/1327] Minimal change to expose segment indices in DefaultDashChunkSource Issue: #3037 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164614478 --- .../source/dash/DefaultDashChunkSource.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 297052f65a..dd62d47621 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 @@ -85,14 +85,14 @@ public class DefaultDashChunkSource implements DashChunkSource { private final int[] adaptationSetIndices; private final TrackSelection trackSelection; private final int trackType; - private final RepresentationHolder[] representationHolders; private final DataSource dataSource; private final long elapsedRealtimeOffsetMs; private final int maxSegmentsPerLoad; + protected final RepresentationHolder[] representationHolders; + private DashManifest manifest; private int periodIndex; - private IOException fatalError; private boolean missingLastSegment; @@ -377,9 +377,12 @@ public class DefaultDashChunkSource implements DashChunkSource { // Protected classes. + /** + * Holds information about a single {@link Representation}. + */ protected static final class RepresentationHolder { - public final ChunkExtractorWrapper extractorWrapper; + /* package */ final ChunkExtractorWrapper extractorWrapper; public Representation representation; public DashSegmentIndex segmentIndex; @@ -387,7 +390,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private long periodDurationUs; private int segmentNumShift; - public RepresentationHolder(long periodDurationUs, Representation representation, + /* package */ RepresentationHolder(long periodDurationUs, Representation representation, boolean enableEventMessageTrack, boolean enableCea608Track) { this.periodDurationUs = periodDurationUs; this.representation = representation; @@ -417,8 +420,8 @@ public class DefaultDashChunkSource implements DashChunkSource { segmentIndex = representation.getIndex(); } - public void updateRepresentation(long newPeriodDurationUs, Representation newRepresentation) - throws BehindLiveWindowException{ + /* package */ void updateRepresentation(long newPeriodDurationUs, + Representation newRepresentation) throws BehindLiveWindowException { DashSegmentIndex oldIndex = representation.getIndex(); DashSegmentIndex newIndex = newRepresentation.getIndex(); From bc0829503acd094d58fecb6bde64f4b734654c5d Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 9 Aug 2017 03:21:25 -0700 Subject: [PATCH 0077/1327] Clean up terminology for MediaSession extension ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164705750 --- extensions/mediasession/README.md | 8 ++++---- .../ext/mediasession/MediaSessionConnector.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/mediasession/README.md b/extensions/mediasession/README.md index 3acf8e4c79..60fec9fb60 100644 --- a/extensions/mediasession/README.md +++ b/extensions/mediasession/README.md @@ -2,10 +2,10 @@ ## Description ## -The MediaSession extension mediates between an ExoPlayer instance and a -[MediaSession][]. It automatically retrieves and implements playback actions -and syncs the player state with the state of the media session. The behaviour -can be extended to support other playback and custom actions. +The MediaSession extension mediates between a Player (or ExoPlayer) instance +and a [MediaSession][]. It automatically retrieves and implements playback +actions and syncs the player state with the state of the media session. The +behaviour can be extended to support other playback and custom actions. [MediaSession]: https://developer.android.com/reference/android/support/v4/media/session/MediaSessionCompat.html diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 0e839b8083..33807930a5 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -47,7 +47,7 @@ import java.util.Map; * Connects a {@link MediaSessionCompat} to a {@link Player}. *

    * The connector listens for actions sent by the media session's controller and implements these - * actions by calling appropriate ExoPlayer methods. The playback state of the media session is + * actions by calling appropriate player methods. The playback state of the media session is * automatically synced with the player. The connector can also be optionally extended by providing * various collaborators: *